Initial commit

This commit is contained in:
2025-05-18 18:02:44 +02:00
commit 3e51315a71
10 changed files with 298 additions and 0 deletions

View File

@@ -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

104
.gitignore vendored Normal file
View File

@@ -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

8
.idea/.gitignore generated vendored Normal file
View File

@@ -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

View File

@@ -0,0 +1,10 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="GoDfaErrorMayBeNotNil" enabled="true" level="WARNING" enabled_by_default="true">
<functions>
<function importPath="net/http" name="Get" />
</functions>
</inspection_tool>
</profile>
</component>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/youtubethumbnails.iml" filepath="$PROJECT_DIR$/.idea/youtubethumbnails.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

9
.idea/youtubethumbnails.iml generated Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

11
.ko.yaml Normal file
View File

@@ -0,0 +1,11 @@
defaultBaseImage: cgr.dev/chainguard/static
defaultPlatforms:
- linux/arm64
- linux/amd64
- linux/arm/v7
builds:
- id: youtubethumbnails
main: .
ldflags:
- -s -w

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module youtubethumbnails
go 1.24

109
main.go Normal file
View File

@@ -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.")
}