Первая версия (CLI-only) #1
@@ -23,6 +23,7 @@ import (
|
||||
"git.weirdcat.su/weirdcat/auto-attendance/internal/config"
|
||||
"git.weirdcat.su/weirdcat/auto-attendance/internal/linkvalidator"
|
||||
"git.weirdcat.su/weirdcat/auto-attendance/internal/logger"
|
||||
"git.weirdcat.su/weirdcat/auto-attendance/internal/screencapturer"
|
||||
"go.uber.org/fx"
|
||||
)
|
||||
|
||||
@@ -32,9 +33,10 @@ func main() {
|
||||
config.NewConfig,
|
||||
logger.NewLogger,
|
||||
linkvalidator.NewLinkValidator,
|
||||
screencapturer.NewWholeScreenCapturer,
|
||||
),
|
||||
fx.Invoke(func(log *logger.Logger) {
|
||||
log.Info("Starting application...");
|
||||
fx.Invoke(func(log *logger.Logger, capturer screencapturer.ScreenCapturer) {
|
||||
log.Debug("starting application...")
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -6,7 +6,12 @@ require github.com/spf13/viper v1.21.0
|
||||
|
||||
require (
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/gen2brain/shm v0.1.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/jezek/xgb v1.1.1 // indirect
|
||||
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018 // indirect
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
|
||||
11
src/go.sum
11
src/go.sum
@@ -1,7 +1,17 @@
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gen2brain/shm v0.1.0 h1:MwPeg+zJQXN0RM9o+HqaSFypNoNEcNpeoGp0BTSx2YY=
|
||||
github.com/gen2brain/shm v0.1.0/go.mod h1:UgIcVtvmOu+aCJpqJX7GOtiN7X2ct+TKLg4RTxwPIUA=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
|
||||
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
|
||||
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018 h1:NQYgMY188uWrS+E/7xMVpydsI48PMHcc7SfR4OxkDF4=
|
||||
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018/go.mod h1:Pmpz2BLf55auQZ67u3rvyI2vAQvNetkK/4zYUmpauZQ=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||
@@ -28,6 +38,7 @@ go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
package screencapturer
|
||||
|
||||
type ScreenCapturer interface {
|
||||
Init() error
|
||||
Start() error
|
||||
Stop() error
|
||||
Screenshot() (filepath string)
|
||||
Get() (filepath string, err error)
|
||||
}
|
||||
|
||||
218
src/internal/screencapturer/wholescreencapturer.go
Normal file
218
src/internal/screencapturer/wholescreencapturer.go
Normal file
@@ -0,0 +1,218 @@
|
||||
// Copyright (c) 2025 Nikolai Papin
|
||||
//
|
||||
// This file is part of the Auto Attendance app that looks for
|
||||
// self-attend QR-codes during lectures and opens their URLs in your
|
||||
// browser.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
// the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package screencapturer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.weirdcat.su/weirdcat/auto-attendance/internal/config"
|
||||
"git.weirdcat.su/weirdcat/auto-attendance/internal/constants"
|
||||
"git.weirdcat.su/weirdcat/auto-attendance/internal/logger"
|
||||
"github.com/kbinani/screenshot"
|
||||
"go.uber.org/fx"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotInitialized = errors.New("wholescreencapturer was not initialized")
|
||||
)
|
||||
|
||||
type wholeScreenCapturer struct {
|
||||
config *config.Config
|
||||
log *logger.Logger
|
||||
|
||||
displayIndex int
|
||||
displayBounds image.Rectangle
|
||||
interval int
|
||||
bufferCount int
|
||||
tempDirectory string
|
||||
initialized bool
|
||||
running bool
|
||||
|
||||
files []string
|
||||
latest string
|
||||
done chan struct{}
|
||||
sync.WaitGroup
|
||||
}
|
||||
|
||||
// Get implements ScreenCapturer.
|
||||
func (w *wholeScreenCapturer) Get() (filepath string, err error) {
|
||||
if !w.initialized {
|
||||
return "", ErrNotInitialized
|
||||
}
|
||||
if w.latest == "" {
|
||||
return "", errors.New("no screenshot captured yet")
|
||||
}
|
||||
return w.latest, nil
|
||||
}
|
||||
|
||||
// Init implements ScreenCapturer.
|
||||
func (w *wholeScreenCapturer) Init() (err error) {
|
||||
|
||||
displayCount := screenshot.NumActiveDisplays()
|
||||
w.displayIndex = w.config.Screenshot.ScreenIndex % displayCount
|
||||
w.log.Debug("detected displays", "count", displayCount, "selected", w.displayIndex)
|
||||
|
||||
w.displayBounds = screenshot.GetDisplayBounds(w.displayIndex)
|
||||
w.log.Debug(
|
||||
"display bounds set",
|
||||
"dx", w.displayBounds.Dx(),
|
||||
"dy", w.displayBounds.Dy(),
|
||||
)
|
||||
|
||||
w.interval = w.config.Screenshot.Interval
|
||||
w.log.Debug("screenshot interval set", "interval", w.interval)
|
||||
|
||||
w.bufferCount = w.config.Screenshot.BufferCount
|
||||
w.log.Debug("screenshot buffer count set", "count", w.bufferCount)
|
||||
|
||||
if w.tempDirectory, err = w.createTempDirectory(); err != nil {
|
||||
w.log.Error("failed to create temporary directory", "error", err)
|
||||
return err
|
||||
}
|
||||
w.log.Debug("temporary directory created", "path", w.tempDirectory)
|
||||
|
||||
w.files = make([]string, 0, w.bufferCount)
|
||||
w.latest = ""
|
||||
w.initialized = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start implements ScreenCapturer.
|
||||
func (w *wholeScreenCapturer) Start() error {
|
||||
if !w.initialized {
|
||||
return ErrNotInitialized
|
||||
}
|
||||
if w.running {
|
||||
w.log.Debug("wholescreencapturer is already running, ignoring start")
|
||||
return nil
|
||||
}
|
||||
w.running = true
|
||||
w.Go(func() {
|
||||
w.captureLoop()
|
||||
})
|
||||
w.log.Debug("wholescreencapturer started")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop implements ScreenCapturer.
|
||||
func (w *wholeScreenCapturer) Stop() error {
|
||||
if !w.initialized {
|
||||
return ErrNotInitialized
|
||||
}
|
||||
if !w.running {
|
||||
w.log.Debug("wholescreencapturer is not running, ignoring stop")
|
||||
return nil
|
||||
}
|
||||
w.log.Debug("stopping wholescreencapturer")
|
||||
close(w.done)
|
||||
w.Wait()
|
||||
w.running = false
|
||||
w.log.Debug("wholescreencapturer stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *wholeScreenCapturer) captureLoop() {
|
||||
ticker := time.NewTicker(time.Duration(w.interval) * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-w.done:
|
||||
w.log.Debug("capture loop stopped")
|
||||
return
|
||||
case <-ticker.C:
|
||||
fp, err := w.captureAndSave()
|
||||
if err != nil {
|
||||
w.log.Error("screenshot capture failed", "error", err)
|
||||
continue
|
||||
}
|
||||
w.addToBuffer(fp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *wholeScreenCapturer) captureAndSave() (string, error) {
|
||||
img, err := screenshot.CaptureRect(w.displayBounds)
|
||||
if err != nil {
|
||||
w.log.Error("failed to capture screenshot", "error", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
filename := fmt.Sprintf("%d.png", now)
|
||||
filePath := filepath.Join(w.tempDirectory, filename)
|
||||
|
||||
file, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
w.log.Error("failed to create screenshot file", "path", filePath, "error", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
err = png.Encode(file, img)
|
||||
if err != nil {
|
||||
w.log.Error("failed to encode image into file", "path", filePath, "error", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
w.log.Debug("Screenshot saved", "path", filePath)
|
||||
return filePath, nil
|
||||
}
|
||||
|
||||
func (w *wholeScreenCapturer) addToBuffer(fp string) {
|
||||
w.files = append(w.files, fp)
|
||||
w.latest = fp
|
||||
|
||||
if len(w.files) > w.bufferCount {
|
||||
old := w.files[0]
|
||||
if err := os.Remove(old); err != nil {
|
||||
w.log.Warn("failed to remove old screenshot", "path", old, "error", err)
|
||||
} else {
|
||||
w.log.Debug("removed old screenshot", "path", old)
|
||||
}
|
||||
w.files = w.files[1:]
|
||||
}
|
||||
}
|
||||
|
||||
func (w *wholeScreenCapturer) createTempDirectory() (path string, err error) {
|
||||
return os.MkdirTemp(w.config.Screenshot.Directory, constants.AppName+"-*")
|
||||
}
|
||||
|
||||
func NewWholeScreenCapturer(lc fx.Lifecycle, config *config.Config, log *logger.Logger) ScreenCapturer {
|
||||
capturer := &wholeScreenCapturer{
|
||||
config: config,
|
||||
log: log,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
lc.Append(fx.StopHook(func(ctx context.Context) error {
|
||||
log.Debug("Stopping capturer (fx hook)")
|
||||
return capturer.Stop()
|
||||
}))
|
||||
return capturer
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright (c) 2025 Nikolai Papin
|
||||
//
|
||||
// This file is part of the Auto Attendance app that looks for
|
||||
// self-attend QR-codes during lectures and opens their URLs in your
|
||||
// browser.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
// the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package screenshotter
|
||||
Reference in New Issue
Block a user