39 Commits

Author SHA1 Message Date
Pablu23
282e2e6173 Dont commit key and cert 2024-07-13 15:35:51 +02:00
Pablu23
2f224e75c5 Halved http requests for images 2024-06-09 21:36:35 +02:00
Pablu23
45a0d9c150 Added automatic tls cert updating 2024-06-09 19:41:49 +02:00
Pablu23
b3fcdfd174 Merge pull request #1 from Tungor/master
Updated login view
2024-06-08 21:25:33 +02:00
Tungor
99c2e4495d Updated login view 2024-06-08 21:22:57 +02:00
Pablu23
48b2e6787b Add Archive, archived Mangas dont get updated 2024-06-08 20:57:20 +02:00
Pablu23
05ca6c9b1b Fix routes not being registered 2024-06-04 21:01:14 +02:00
Pablu23
b0c204ec68 Fix problems with new Options 2024-06-04 20:58:59 +02:00
Pablu23
8f65d81015 Add more flags to customize auth 2024-06-04 20:51:06 +02:00
Pablu23
dbe349d0e6 Add server Options and rework flags a bit 2024-06-04 20:45:45 +02:00
Pablu23
b46d00a873 Added file logging 2024-05-31 00:23:15 +02:00
Pablu23
e5e7c4eb54 Gorm silent, except in debug mode 2024-05-30 23:58:18 +02:00
Pablu23
766da5aeb2 Added better logging 2024-05-30 23:42:35 +02:00
Pablu23
2aab7906e1 Changed login site input field to password input type 2024-05-30 22:41:12 +02:00
Pablu23
124dab097a Fixed theme 2024-05-30 20:40:55 +02:00
Pablu23
16be88a849 Add Update Button 2024-05-30 20:36:03 +02:00
Pablu23
8de5bb0fdc Added interval flag 2024-05-30 20:30:45 +02:00
Pablu23
e9c7c6f915 Cleared up flag code a bit 2024-05-30 20:01:59 +02:00
Pablu23
11857a7156 Added TLS 2024-05-30 19:36:33 +02:00
Pablu23
a2232026a0 Added Flags 2024-05-22 23:00:50 +02:00
Pablu23
f905d482a5 Fixed middleware 2024-05-22 22:16:50 +02:00
Pablu23
392114b240 Removed binary 2024-05-21 17:14:01 +02:00
Pablu23
63ffb8df6e Added most basic auth 2024-05-21 16:36:40 +02:00
Pablu23
0904a1214e Changed makefile to reflect Project structure changes 2024-05-18 14:25:24 +02:00
Pablu23
bf4b4a3a15 Changed Project structure for non Package Project 2024-05-18 14:21:56 +02:00
Pablu23
1bd80bc1c2 Change own DbTable implementation for Gorm 2024-05-14 11:28:49 +02:00
Pablu23
620a043286 minor changes to table.go 2024-04-04 21:36:12 +02:00
Pablu23
56fc884952 Changed module name to github Path, and minor changes 2024-04-04 09:55:29 +02:00
Pablu23
58344d4def Fixed various Bugs 2024-04-03 16:12:44 +02:00
Pablu23
cb592c7109 Added Settings for Manga ordering, fixed theme selector 2024-04-03 13:13:08 +02:00
Pablu23
f712211194 Added different Ports for release and develop, changed out Sort for simple iteration (Should be faster), Fixed Theme Switcher, fixed Saving of read chapters 2024-04-02 19:27:25 +02:00
Pablu23
e3e0da86fc Supress Theme warning 2024-04-02 19:06:13 +02:00
Pablu23
e45109fcd5 Added initial Settings 2024-04-02 19:05:36 +02:00
Pablu23
c83a10823d Significantly simplified adding new Database Tables 2024-04-02 18:22:54 +02:00
Pablu23
ce878efce3 Added start of db rework, also added Qol changes 2024-04-02 16:35:50 +02:00
Pablu23
1377fd420e Added saving of LatestAvailableChapter and updating it in goroutine every 5 Minutes, also Added filtering to menu 2024-03-06 23:27:14 +01:00
Pablu23
ad1fcbc68a Now actually use Thumbnail that was loaded from db, instead of re downloading it every time. Also save Thumbnail to Manga if it couldnt be saved before 2024-03-01 16:46:19 +01:00
Pablu23
b7f2a389cd Added chaching, and significantly improved Menu loading times after first 2024-03-01 14:54:25 +01:00
Pablu23
a8ba8728dc Added Last Chapter functionality to see how many more chapters you have, also kinda works as update Status bar
changed it to only load latest chapter instead of all chapters for manga
Added develop and release db from different locations
Added stopwatch for future performance improvement metrics
2024-03-01 14:31:38 +01:00
28 changed files with 1308 additions and 629 deletions

9
.gitignore vendored
View File

@@ -2,4 +2,11 @@
findings.txt
test.txt
h.html
db.sqlite
*.sqlite
*.bak
/bin
*.exe
*secret*
mangaGetter
*.crt
*.key

View File

@@ -1,11 +1,8 @@
run: develop
bin/develop
bin/develop --secret test --server --port 8181 --database db.sqlite --debug --pretty
develop:
go build -tags Develop -o bin/develop cmd/mangaGetter/main.go
go build -tags Develop -o bin/develop
release:
go build -o bin/MangaGetter_unix cmd/mangaGetter/main.go
go build -o bin/MangaGetter_unix
win-amd64:
GOOS=windows GOARCH=amd64 go build -o bin/MangaGetter-amd64_windows.exe cmd/mangaGetter/main.go
GOOS=windows GOARCH=amd64 go build -o bin/MangaGetter-amd64_windows.exe

View File

@@ -8,10 +8,11 @@ That's, why I created this simple pre Loader, right now it's not really user-fri
a few more features
# Features that might get added:
- Manga / Chapter History
- Searchbar
- Better looking UI
- Main Screen
- Genres and Filter
- More Providers like Asuratoon
- Performance improvements
# Pretext

View File

@@ -1,123 +0,0 @@
package main
import (
"fmt"
"mangaGetter/internal/database"
"mangaGetter/internal/provider"
"mangaGetter/internal/server"
"net/http"
"os"
"os/exec"
"os/signal"
"path/filepath"
"runtime"
"time"
)
func main() {
dir, err := os.UserCacheDir()
if err != nil {
fmt.Println(nil)
return
}
dirPath := filepath.Join(dir, "MangaGetter")
filePath := filepath.Join(dirPath, "db.sqlite")
if _, err := os.Stat(dirPath); os.IsNotExist(err) {
err = os.Mkdir(dirPath, os.ModePerm)
if err != nil {
fmt.Println(err)
return
}
}
if _, err := os.Stat(filePath); os.IsNotExist(err) {
f, err := os.Create(filePath)
if err != nil {
fmt.Println(err)
return
}
err = f.Close()
if err != nil {
fmt.Println(err)
return
}
}
db := database.NewDatabase(filePath, true)
err = db.Open()
if err != nil {
fmt.Println(err)
return
}
s := server.New(&provider.Bato{}, &db)
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
for range c {
Close(&db)
}
}()
http.HandleFunc("/", s.HandleMenu)
http.HandleFunc("/new/", s.HandleNewQuery)
http.HandleFunc("/new/title/{title}/{chapter}", s.HandleNew)
http.HandleFunc("/current/", s.HandleCurrent)
http.HandleFunc("/img/{url}/", s.HandleImage)
http.HandleFunc("POST /next", s.HandleNext)
http.HandleFunc("POST /prev", s.HandlePrev)
http.HandleFunc("POST /exit", s.HandleExit)
http.HandleFunc("POST /delete", s.HandleDelete)
http.HandleFunc("/favicon.ico", s.HandleFavicon)
go func() {
time.Sleep(300 * time.Millisecond)
err := open("http://localhost:8000")
if err != nil {
fmt.Println(err)
}
}()
fmt.Println("Server starting...")
err = http.ListenAndServe(":8000", nil)
if err != nil {
fmt.Println(err)
return
}
}
func open(url string) error {
var cmd string
var args []string
switch runtime.GOOS {
case "windows":
cmd = "cmd"
args = []string{"/c", "start"}
case "darwin":
cmd = "open"
default: // "linux", "freebsd", "openbsd", "netbsd"
cmd = "xdg-open"
}
args = append(args, url)
return exec.Command(cmd, args...).Start()
}
func Close(db *database.Manager) {
fmt.Println("Attempting to save and close DB")
err := db.Save()
if err != nil {
fmt.Println(err)
return
}
err = db.Close()
if err != nil {
fmt.Println(err)
}
os.Exit(0)
}

15
develop.go Normal file
View File

@@ -0,0 +1,15 @@
//go:build Develop
package main
func getSecretPath() (string, error) {
return "", nil
}
func getSecret() (string, error) {
return "test", nil
}
func getDbPath() string {
return "db.sqlite"
}

18
go.mod
View File

@@ -1,4 +1,4 @@
module mangaGetter
module github.com/pablu23/mangaGetter
go 1.22
@@ -6,3 +6,19 @@ require (
github.com/mattn/go-sqlite3 v1.14.22
golang.org/x/text v0.14.0
)
require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/rs/zerolog v1.33.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/sys v0.20.0 // indirect
)
require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gorm.io/driver/sqlite v1.5.5
gorm.io/gorm v1.25.10
)

31
go.sum
View File

@@ -1,4 +1,35 @@
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E=
gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE=
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=

View File

@@ -0,0 +1,21 @@
package database
type Chapter struct {
Id int `gorm:"primary_key;AUTO_INCREMENT"`
Url string
Name string
Number string
TimeStampUnix int64
MangaId int
}
func NewChapter(id int, mangaId int, url string, name string, number string, timeStampUnix int64) Chapter {
return Chapter{
Id: id,
Url: url,
Name: name,
Number: number,
TimeStampUnix: timeStampUnix,
MangaId: mangaId,
}
}

View File

@@ -1,16 +0,0 @@
create table if not exists Manga (
ID integer not null primary key,
Title text,
TimeStampUnixEpoch int,
Thumbnail blob
);
create table if not exists Chapter (
ID integer not null primary key,
MangaID integer not null,
Url text not null,
Name text null,
Number int not null,
TimeStampUnixEpoch int,
foreign key(MangaID) references Manga(ID)
);

View File

@@ -1,222 +1,75 @@
package database
import (
"bytes"
"database/sql"
_ "embed"
"fmt"
"sync"
_ "github.com/mattn/go-sqlite3"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
type Manga struct {
Id int
Title string
TimeStampUnix int64
Thumbnail *bytes.Buffer
// Not in DB
LatestChapter *Chapter
}
type Chapter struct {
Id int
Manga *Manga
Url string
Name string
Number int
TimeStampUnix int64
}
type Manager struct {
ConnectionString string
db *sql.DB
Rw *sync.Mutex
Mangas map[int]*Manga
Chapters map[int]*Chapter
CreateIfNotExists bool
ConnectionString string
Db *gorm.DB
CreateIfNotExists bool
ActivateGormLogger bool
}
func NewDatabase(connectionString string, createIfNotExists bool) Manager {
func NewDatabase(connectionString string, createIfNotExists bool, activateGormLogger bool) Manager {
return Manager{
ConnectionString: connectionString,
Rw: &sync.Mutex{},
Mangas: make(map[int]*Manga),
Chapters: make(map[int]*Chapter),
CreateIfNotExists: createIfNotExists,
ConnectionString: connectionString,
Db: nil,
CreateIfNotExists: createIfNotExists,
ActivateGormLogger: activateGormLogger,
}
}
func (dbMgr *Manager) Open() error {
db, err := sql.Open("sqlite3", dbMgr.ConnectionString)
if err != nil {
return err
var db *gorm.DB
var err error
if dbMgr.ActivateGormLogger {
db, err = gorm.Open(sqlite.Open(dbMgr.ConnectionString), &gorm.Config{})
if err != nil {
return err
}
} else {
db, err = gorm.Open(sqlite.Open(dbMgr.ConnectionString), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
return err
}
}
dbMgr.db = db
dbMgr.Db = db
if dbMgr.CreateIfNotExists {
err = dbMgr.createDatabaseIfNotExists()
if err != nil {
return err
}
}
err = dbMgr.load()
return err
}
func (dbMgr *Manager) Close() error {
err := dbMgr.db.Close()
sql, err := dbMgr.Db.DB()
if err != nil {
return err
}
dbMgr.Mangas = nil
dbMgr.Chapters = nil
dbMgr.db = nil
return nil
}
func (dbMgr *Manager) Delete(mangaId int) error {
db := dbMgr.db
fmt.Println("Locking Rw in database.go:84")
dbMgr.Rw.Lock()
defer func() {
fmt.Println("Unlocking Rw in database.go:87")
dbMgr.Rw.Unlock()
}()
_, err := db.Exec("DELETE from Chapter where MangaID = ?", mangaId)
err = sql.Close()
if err != nil {
return err
}
_, err = db.Exec("DELETE from Manga where ID = ?", mangaId)
if err != nil {
return err
}
for i, chapter := range dbMgr.Chapters {
if chapter.Manga.Id == mangaId {
delete(dbMgr.Chapters, i)
}
}
delete(dbMgr.Mangas, mangaId)
dbMgr.Db = nil
return nil
}
func (dbMgr *Manager) Save() error {
db := dbMgr.db
fmt.Println("Locking Rw in database.go:113")
dbMgr.Rw.Lock()
defer func() {
fmt.Println("Unlocking Rw in database.go:116")
dbMgr.Rw.Unlock()
}()
for _, m := range dbMgr.Mangas {
count := 0
err := db.QueryRow("SELECT COUNT(*) FROM Manga where ID = ?", m.Id).Scan(&count)
if err != nil {
return err
}
if count == 0 {
if m.Thumbnail != nil {
_, err := db.Exec("INSERT INTO Manga(ID, Title, TimeStampUnixEpoch, Thumbnail) values(?, ?, ?, ?)", m.Id, m.Title, m.TimeStampUnix, m.Thumbnail.Bytes())
if err != nil {
return err
}
} else {
_, err := db.Exec("INSERT INTO Manga(ID, Title, TimeStampUnixEpoch ) values(?, ?, ?)", m.Id, m.Title, m.TimeStampUnix)
if err != nil {
return err
}
}
} else {
_, err := db.Exec("UPDATE Manga set Title = ?, TimeStampUnixEpoch = ? WHERE ID = ?", m.Title, m.TimeStampUnix, m.Id)
if err != nil {
return err
}
}
}
for _, c := range dbMgr.Chapters {
count := 0
err := db.QueryRow("SELECT COUNT(*) FROM Chapter where ID = ?", c.Id).Scan(&count)
if err != nil {
return err
}
if count == 0 {
_, err := db.Exec("INSERT INTO Chapter(ID, MangaID, Url, Name, Number, TimeStampUnixEpoch) VALUES (?, ?, ?, ?, ?, ?)", c.Id, c.Manga.Id, c.Url, c.Name, c.Number, c.TimeStampUnix)
if err != nil {
return err
}
} else {
_, err = db.Exec("UPDATE Chapter set Name = ?, Url = ?, Number = ?, TimeStampUnixEpoch = ? where ID = ?", c.Name, c.Url, c.Number, c.TimeStampUnix, c.Id)
if err != nil {
return err
}
}
}
return nil
func (dbMgr *Manager) Delete(mangaId int) {
dbMgr.Db.Delete(&Manga{}, mangaId)
}
//go:embed createDb.sql
var createSql string
func (dbMgr *Manager) createDatabaseIfNotExists() error {
_, err := dbMgr.db.Exec(createSql)
err := dbMgr.Db.AutoMigrate(&Manga{}, &Chapter{}, &Setting{})
return err
}
func (dbMgr *Manager) load() error {
db := dbMgr.db
fmt.Println("Locking Rw in database.go:180")
dbMgr.Rw.Lock()
defer func() {
fmt.Println("Unlocking Rw in database.go:183")
dbMgr.Rw.Unlock()
}()
rows, err := db.Query("SELECT * FROM Manga")
if err != nil {
return err
}
for rows.Next() {
manga := Manga{}
var thumbnail []byte
if err = rows.Scan(&manga.Id, &manga.Title, &manga.TimeStampUnix, &thumbnail); err != nil {
return err
}
manga.Thumbnail = bytes.NewBuffer(thumbnail)
dbMgr.Mangas[manga.Id] = &manga
}
rows, err = db.Query("SELECT * FROM Chapter")
if err != nil {
return err
}
for rows.Next() {
chapter := Chapter{}
var mangaID int
if err = rows.Scan(&chapter.Id, &mangaID, &chapter.Url, &chapter.Name, &chapter.Number, &chapter.TimeStampUnix); err != nil {
return err
}
chapter.Manga = dbMgr.Mangas[mangaID]
if dbMgr.Mangas[mangaID].LatestChapter == nil || dbMgr.Mangas[mangaID].LatestChapter.TimeStampUnix < chapter.TimeStampUnix {
dbMgr.Mangas[mangaID].LatestChapter = &chapter
}
dbMgr.Chapters[chapter.Id] = &chapter
}
return nil
}

View File

@@ -0,0 +1,49 @@
package database
type Manga struct {
Id int `gorm:"primary_key;AUTO_INCREMENT"`
Title string
TimeStampUnix int64
Thumbnail []byte
LastChapterNum string
Chapters []Chapter
Enabled bool
//`gorm:"foreignkey:MangaID"`
}
func NewManga(id int, title string, timeStampUnix int64) Manga {
return Manga{
Id: id,
Title: title,
TimeStampUnix: timeStampUnix,
LastChapterNum: "",
Enabled: true,
}
}
// GetLatestChapter TODO: Cache this somehow
func (m *Manga) GetLatestChapter() (*Chapter, bool) {
highest := int64(0)
index := 0
for i, chapter := range m.Chapters {
if chapter.MangaId == m.Id && highest < chapter.TimeStampUnix {
highest = chapter.TimeStampUnix
index = i
}
}
if highest == 0 {
return nil, false
}
return &m.Chapters[index], true
//result := db.Where("manga.id = ?", m.Id).Order("TimeStampUnix desc").Take(&chapter)
//if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
// return &chapter, true, result.Error
//} else if errors.Is(result.Error, gorm.ErrRecordNotFound) {
// return &chapter, false, nil
//} else {
// return &chapter, true, nil
//}
}

View File

@@ -0,0 +1,27 @@
package database
type Setting struct {
Name string `gorm:"PRIMARY_KEY"`
Value string
Default string
}
func NewSetting(name string, defaultValue string) Setting {
return Setting{
Name: name,
Value: defaultValue,
Default: defaultValue,
}
}
//func initSettings(settings *DbTable[string, Setting]) {
// addSettingIfNotExists("theme", "white", settings)
// addSettingIfNotExists("order", "title", settings)
//}
//
//func addSettingIfNotExists(name string, value string, settings *DbTable[string, Setting]) {
// _, exists := settings.Get(name)
// if !exists {
// settings.Set(name, NewSetting(name, value))
// }
//}

View File

@@ -7,10 +7,19 @@ import (
"net/http"
"regexp"
"strconv"
"strings"
"github.com/rs/zerolog/log"
)
type Bato struct{}
func (b *Bato) CleanUrlToSub(url string) string {
trimmed := strings.TrimPrefix(url, "https://bato.to/title")
trimmed = strings.Trim(trimmed, "/")
return trimmed
}
func (b *Bato) GetImageList(html string) ([]string, error) {
reg, err := regexp.Compile(`<astro-island.*props=".*;imageFiles&quot;:\[1,&quot;\[(.*)]&quot;]`)
if err != nil {
@@ -49,7 +58,7 @@ func (b *Bato) GetHtml(titleSubUrl string) (string, error) {
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
fmt.Printf("Could not close body because: %v\n", err)
log.Error().Err(err).Msg("Could not close http body")
}
}(resp.Body)
@@ -64,14 +73,23 @@ func (b *Bato) GetHtml(titleSubUrl string) (string, error) {
func (b *Bato) GetNext(html string) (subUrl string, err error) {
reg, err := regexp.Compile(`<a data-hk="0-6-0" .*? href="(.*?)["']`)
match := reg.FindStringSubmatch(html)
if err != nil {
return "", err
}
match := reg.FindStringSubmatch(html)
if len(match) <= 1 {
return "", err
}
return match[1], err
}
func (b *Bato) GetPrev(html string) (subUrl string, err error) {
reg, err := regexp.Compile(`<a data-hk="0-5-0" .*? href="(.*?)["']`)
match := reg.FindStringSubmatch(html)
if len(match) <= 1 {
return "", err
}
return match[1], err
}
@@ -109,9 +127,24 @@ func (b *Bato) GetTitleIdAndChapterId(url string) (titleId int, chapterId int, e
return t, c, err
}
//func (b *Bato) GetChapterList(url string) (chapterIds []int, err error) {
//
//}
func (b *Bato) GetChapterList(subUrl string) (subUrls []string, err error) {
reg, err := regexp.Compile(`<div class="space-x-1">.*?<a href="(.*?)" .*?>.*?</a>`)
if err != nil {
return nil, err
}
html, err := b.GetHtml(subUrl)
if err != nil {
return nil, err
}
subUrls = make([]string, 0)
matches := reg.FindAllStringSubmatch(html, -1)
for _, match := range matches {
subUrls = append(subUrls, match[1])
}
return subUrls, nil
}
func (b *Bato) GetThumbnail(subUrl string) (thumbnailUrl string, err error) {
url := fmt.Sprintf("https://bato.to/title/%s", subUrl)

View File

@@ -1,6 +1,7 @@
package provider
type Provider interface {
CleanUrlToSub(url string) string
GetImageList(html string) (imageUrls []string, err error)
GetHtml(url string) (html string, err error)
GetNext(html string) (url string, err error)
@@ -8,4 +9,5 @@ type Provider interface {
GetTitleAndChapter(url string) (title string, chapter string, err error)
GetTitleIdAndChapterId(url string) (titleId int, chapterId int, err error)
GetThumbnail(mangaId string) (thumbnailUrl string, err error)
GetChapterList(url string) (urls []string, err error)
}

View File

@@ -1,31 +1,73 @@
package server
import (
"bytes"
"cmp"
_ "embed"
"errors"
"fmt"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"html/template"
"mangaGetter/internal/database"
"mangaGetter/internal/view"
"net/http"
"slices"
"strconv"
"strings"
"time"
"github.com/pablu23/mangaGetter/internal/database"
"github.com/pablu23/mangaGetter/internal/view"
"github.com/rs/zerolog/log"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"gorm.io/gorm"
)
func (s *Server) HandleDisable(w http.ResponseWriter, r *http.Request) {
id := r.PostFormValue("mangaId")
var manga database.Manga
s.DbMgr.Db.Where("id = ?", id).First(&manga)
if manga.Enabled {
http.Redirect(w, r, "/", http.StatusFound)
} else {
http.Redirect(w, r, "/archive", http.StatusFound)
}
manga.Enabled = !manga.Enabled
s.DbMgr.Db.Save(&manga)
}
func (s *Server) HandleUpdate(w http.ResponseWriter, r *http.Request) {
s.UpdateMangaList()
http.Redirect(w, r, "/", http.StatusFound)
}
func (s *Server) HandleLoginPost(w http.ResponseWriter, r *http.Request) {
if s.options.Auth.Enabled {
auth := s.options.Auth.Get()
secret := r.PostFormValue("secret")
http.SetCookie(w, &http.Cookie{
Name: "auth",
Value: secret,
Path: "/",
MaxAge: auth.MaxAge,
Secure: auth.Secure,
HttpOnly: false,
SameSite: http.SameSiteLaxMode,
})
}
http.Redirect(w, r, "/", http.StatusFound)
}
func (s *Server) HandleLogin(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(view.GetViewTemplate(view.Login))
tmpl.Execute(w, nil)
}
func (s *Server) HandleNew(w http.ResponseWriter, r *http.Request) {
title := r.PathValue("title")
chapter := r.PathValue("chapter")
url := fmt.Sprintf("/title/%s/%s", title, chapter)
s.Mutex.Lock()
s.ImageBuffers = make(map[string]*bytes.Buffer)
s.Mutex.Unlock()
s.CurrSubUrl = url
s.PrevSubUrl = ""
s.NextSubUrl = ""
@@ -34,56 +76,119 @@ func (s *Server) HandleNew(w http.ResponseWriter, r *http.Request) {
go s.LoadNext()
go s.LoadPrev()
http.Redirect(w, r, "/current/", http.StatusTemporaryRedirect)
http.Redirect(w, r, "/current/", http.StatusFound)
}
func (s *Server) HandleArchive(w http.ResponseWriter, r *http.Request) {
var all []*database.Manga
_ = s.DbMgr.Db.Preload("Chapters").Where("enabled = 0").Find(&all)
var tmp []database.Setting
s.DbMgr.Db.Find(&tmp)
settings := make(map[string]database.Setting)
for _, m := range tmp {
settings[m.Name] = m
}
s.ViewMenu(w, all, settings, true)
}
func (s *Server) HandleMenu(w http.ResponseWriter, _ *http.Request) {
var all []*database.Manga
_ = s.DbMgr.Db.Preload("Chapters").Where("enabled = 1").Find(&all)
var tmp []database.Setting
s.DbMgr.Db.Find(&tmp)
settings := make(map[string]database.Setting)
for _, m := range tmp {
settings[m.Name] = m
}
s.ViewMenu(w, all, settings, false)
}
func (s *Server) ViewMenu(w http.ResponseWriter, mangas []*database.Manga, settings map[string]database.Setting, archive bool) {
tmpl := template.Must(view.GetViewTemplate(view.Menu))
fmt.Println("Locking Rw in handler.go:43")
s.DbMgr.Rw.Lock()
defer func() {
fmt.Println("Unlocking Rw in handler.go:46")
s.DbMgr.Rw.Unlock()
}()
all := s.DbMgr.Mangas
l := len(all)
l := len(mangas)
mangaViewModels := make([]view.MangaViewModel, l)
counter := 0
for _, manga := range all {
//TODO: Change all this to be more performant
for _, manga := range mangas {
title := cases.Title(language.English, cases.Compact).String(strings.Replace(manga.Title, "-", " ", -1))
thumbnail, err := s.LoadThumbnail(manga.Id)
thumbnail, updated, err := s.LoadThumbnail(manga)
//TODO: Add default picture instead of not showing Manga at all
if err != nil {
continue
}
manga.Thumbnail = s.ImageBuffers[thumbnail]
if updated {
s.DbMgr.Db.Save(manga)
}
// This is very slow
// TODO: put this into own Method
if manga.LastChapterNum == "" {
err, updated := s.UpdateLatestAvailableChapter(manga)
if err != nil {
log.Error().Err(err).Msg("Could not update latest available chapters")
}
if updated {
s.DbMgr.Db.Save(manga)
}
}
latestChapter, ok := manga.GetLatestChapter()
if !ok {
continue
}
mangaViewModels[counter] = view.MangaViewModel{
ID: manga.Id,
Title: title,
Number: manga.LatestChapter.Number,
ID: manga.Id,
Title: title,
Number: latestChapter.Number,
LastNumber: manga.LastChapterNum,
// I Hate this time Format... 15 = hh, 04 = mm, 02 = DD, 01 = MM, 06 == YY
LastTime: time.Unix(manga.TimeStampUnix, 0).Format("15:04 (02-01-06)"),
Url: manga.LatestChapter.Url,
Url: latestChapter.Url,
ThumbnailUrl: thumbnail,
Enabled: manga.Enabled,
}
counter++
}
slices.SortStableFunc(mangaViewModels, func(a, b view.MangaViewModel) int {
return cmp.Compare(a.Title, b.Title)
})
order, ok := settings["order"]
if !ok || order.Value == "title" {
slices.SortStableFunc(mangaViewModels, func(a, b view.MangaViewModel) int {
return cmp.Compare(a.Title, b.Title)
})
} else if order.Value == "chapter" {
slices.SortStableFunc(mangaViewModels, func(a, b view.MangaViewModel) int {
return cmp.Compare(b.Number, a.Number)
})
} else if order.Value == "last" {
slices.SortStableFunc(mangaViewModels, func(a, b view.MangaViewModel) int {
aT, err := time.Parse("15:04 (02-01-06)", a.LastTime)
if err != nil {
return cmp.Compare(a.Title, b.Title)
}
bT, err := time.Parse("15:04 (02-01-06)", b.LastTime)
if err != nil {
return cmp.Compare(a.Title, b.Title)
}
return bT.Compare(aT)
})
}
menuViewModel := view.MenuViewModel{
Mangas: mangaViewModels,
Settings: settings,
Mangas: mangaViewModels,
Archive: archive,
}
err := tmpl.Execute(w, menuViewModel)
if err != nil {
fmt.Println(err)
log.Error().Err(err).Msg("Could not template Menu")
}
}
@@ -91,93 +196,86 @@ func (s *Server) HandleDelete(w http.ResponseWriter, r *http.Request) {
mangaStr := r.PostFormValue("mangaId")
if mangaStr == "" {
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
http.Redirect(w, r, "/", http.StatusFound)
return
}
mangaId, err := strconv.Atoi(mangaStr)
if err != nil {
fmt.Println(err)
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
log.Error().Err(err).Str("Id", mangaStr).Msg("Could not convert id to int")
http.Redirect(w, r, "/", http.StatusFound)
return
}
err = s.DbMgr.Delete(mangaId)
if err != nil {
fmt.Println(err)
}
s.DbMgr.Delete(mangaId)
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
http.Redirect(w, r, "/", http.StatusFound)
}
func (s *Server) HandleExit(w http.ResponseWriter, r *http.Request) {
err := s.DbMgr.Save()
http.Redirect(w, r, "/", http.StatusFound)
go func() {
s.Mutex.Lock()
if s.PrevViewModel != nil {
for _, img := range s.PrevViewModel.Images {
delete(s.ImageBuffers, img.Path)
}
}
if s.CurrViewModel != nil {
for _, img := range s.CurrViewModel.Images {
delete(s.ImageBuffers, img.Path)
}
}
if s.NextViewModel != nil {
for _, img := range s.NextViewModel.Images {
delete(s.ImageBuffers, img.Path)
}
}
s.Mutex.Unlock()
log.Info().Msg("Cleaned up images")
}()
}
func (s *Server) HandleCurrent(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(view.GetViewTemplate(view.Viewer))
mangaId, chapterId, err := s.Provider.GetTitleIdAndChapterId(s.CurrSubUrl)
if err != nil {
fmt.Println(err)
log.Error().Err(err).Str("subUrl", s.CurrSubUrl).Msg("Could not get TitleId and ChapterId")
http.Redirect(w, r, "/", http.StatusFound)
return
}
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
}
func (s *Server) HandleCurrent(w http.ResponseWriter, _ *http.Request) {
tmpl := template.Must(view.GetViewTemplate(view.Viewer))
fmt.Println("Locking Rw in handler.go:125")
s.DbMgr.Rw.Lock()
defer func() {
fmt.Println("Unlocking Rw in handler.go:128")
s.DbMgr.Rw.Unlock()
}()
mangaId, chapterId, err := s.Provider.GetTitleIdAndChapterId(s.CurrSubUrl)
title, chapterName, err := s.Provider.GetTitleAndChapter(s.CurrSubUrl)
if err != nil {
fmt.Println(err)
} else {
title, chapter, err := s.Provider.GetTitleAndChapter(s.CurrSubUrl)
if err != nil {
fmt.Println(err)
} else {
var manga *database.Manga
if s.DbMgr.Mangas[mangaId] == nil {
manga = &database.Manga{
Id: mangaId,
Title: title,
TimeStampUnix: time.Now().Unix(),
}
s.DbMgr.Mangas[mangaId] = manga
} else {
manga = s.DbMgr.Mangas[mangaId]
s.DbMgr.Mangas[mangaId].TimeStampUnix = time.Now().Unix()
}
if s.DbMgr.Chapters[chapterId] == nil {
chapterNumberStr := strings.Replace(chapter, "ch_", "", 1)
number, err := strconv.Atoi(chapterNumberStr)
if err != nil {
fmt.Println(err)
number = 0
}
s.DbMgr.Chapters[chapterId] = &database.Chapter{
Id: chapterId,
Manga: manga,
Url: s.CurrSubUrl,
Name: chapter,
Number: number,
TimeStampUnix: time.Now().Unix(),
}
} else {
s.DbMgr.Chapters[chapterId].TimeStampUnix = time.Now().Unix()
}
s.DbMgr.Mangas[mangaId].LatestChapter = s.DbMgr.Chapters[chapterId]
}
log.Warn().Err(err).Str("subUrl", s.CurrSubUrl).Msg("Could not get Title and Chapter")
}
var manga database.Manga
result := s.DbMgr.Db.First(&manga, mangaId)
if result.Error != nil && errors.Is(result.Error, gorm.ErrRecordNotFound) {
manga = database.NewManga(mangaId, title, time.Now().Unix())
} else {
manga.TimeStampUnix = time.Now().Unix()
}
var chapter database.Chapter
result = s.DbMgr.Db.First(&chapter, chapterId)
if result.Error != nil && errors.Is(result.Error, gorm.ErrRecordNotFound) {
chapterNumberStr := strings.Replace(chapterName, "ch_", "", 1)
chapter = database.NewChapter(chapterId, mangaId, s.CurrSubUrl, chapterName, chapterNumberStr, time.Now().Unix())
} else {
chapter.TimeStampUnix = time.Now().Unix()
}
s.DbMgr.Db.Save(&manga)
s.DbMgr.Db.Save(&chapter)
err = tmpl.Execute(w, s.CurrViewModel)
if err != nil {
fmt.Println(err)
log.Error().Err(err).Msg("Could not template Current")
}
}
@@ -187,32 +285,30 @@ func (s *Server) HandleImage(w http.ResponseWriter, r *http.Request) {
defer s.Mutex.Unlock()
buf := s.ImageBuffers[u]
if buf == nil {
fmt.Printf("url: %s is nil\n", u)
w.WriteHeader(400)
log.Warn().Str("url", u).Msg("Image not found")
w.WriteHeader(http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "image/webp")
_, err := w.Write(buf.Bytes())
_, err := w.Write(buf)
if err != nil {
fmt.Println(err)
log.Error().Err(err).Msg("Could not write image")
}
}
//go:embed favicon.ico
var ico []byte
func (s *Server) HandleFavicon(w http.ResponseWriter, r *http.Request) {
func (s *Server) HandleFavicon(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "image/webp")
_, err := w.Write(ico)
if err != nil {
fmt.Println(err)
log.Error().Err(err).Msg("Could not write favicon")
}
}
func (s *Server) HandleNext(w http.ResponseWriter, r *http.Request) {
fmt.Println("Received Next")
if s.PrevViewModel != nil {
go func(viewModel view.ImageViewModel, s *Server) {
s.Mutex.Lock()
@@ -220,16 +316,12 @@ func (s *Server) HandleNext(w http.ResponseWriter, r *http.Request) {
delete(s.ImageBuffers, img.Path)
}
s.Mutex.Unlock()
fmt.Println("Cleaned out of scope Last")
log.Debug().Msg("Cleaned imagebuffer")
}(*s.PrevViewModel, s)
}
if s.NextViewModel == nil || s.NextSubUrl == "" {
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
err := s.DbMgr.Save()
if err != nil {
fmt.Println(err)
}
http.Redirect(w, r, "/", http.StatusFound)
return
}
@@ -240,10 +332,10 @@ func (s *Server) HandleNext(w http.ResponseWriter, r *http.Request) {
go s.LoadNext()
http.Redirect(w, r, "/current/", http.StatusTemporaryRedirect)
http.Redirect(w, r, "/current/", http.StatusFound)
}
func (s *Server) HandlePrev(w http.ResponseWriter, r *http.Request) {
fmt.Println("Received Prev")
if s.NextViewModel != nil {
go func(viewModel view.ImageViewModel, s *Server) {
s.Mutex.Lock()
@@ -251,16 +343,12 @@ func (s *Server) HandlePrev(w http.ResponseWriter, r *http.Request) {
delete(s.ImageBuffers, img.Path)
}
s.Mutex.Unlock()
fmt.Println("Cleaned out of scope Last")
log.Debug().Msg("Cleaned imagebuffer")
}(*s.NextViewModel, s)
}
if s.PrevViewModel == nil || s.PrevSubUrl == "" {
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
err := s.DbMgr.Save()
if err != nil {
fmt.Println(err)
}
http.Redirect(w, r, "/", http.StatusFound)
return
}
@@ -271,17 +359,51 @@ func (s *Server) HandlePrev(w http.ResponseWriter, r *http.Request) {
go s.LoadPrev()
http.Redirect(w, r, "/current/", http.StatusTemporaryRedirect)
http.Redirect(w, r, "/current/", http.StatusFound)
}
func (s *Server) HandleSettingSet(w http.ResponseWriter, r *http.Request) {
settingName := r.PathValue("setting")
settingValue := r.PathValue("value")
var setting database.Setting
res := s.DbMgr.Db.First(&setting, "name = ?", settingName)
if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) {
set := database.NewSetting(settingName, settingValue)
s.DbMgr.Db.Save(&set)
} else {
s.DbMgr.Db.Model(&setting).Update("value", settingValue)
}
http.Redirect(w, r, "/", http.StatusFound)
}
func (s *Server) HandleSetting(w http.ResponseWriter, r *http.Request) {
settingName := r.PostFormValue("setting")
settingValue := r.PostFormValue(settingName)
var setting database.Setting
res := s.DbMgr.Db.First(&setting, "name = ?", settingName)
if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) {
set := database.NewSetting(settingName, settingValue)
s.DbMgr.Db.Save(&set)
} else if res.Error != nil {
log.Error().Err(res.Error).Send()
} else {
s.DbMgr.Db.Model(&setting).Update("value", settingValue)
}
http.Redirect(w, r, "/", http.StatusFound)
}
func (s *Server) HandleNewQuery(w http.ResponseWriter, r *http.Request) {
sub := r.PostFormValue("subUrl")
sub = s.Provider.CleanUrlToSub(sub)
url := fmt.Sprintf("/title/%s", sub)
s.Mutex.Lock()
s.ImageBuffers = make(map[string]*bytes.Buffer)
s.Mutex.Unlock()
s.CurrSubUrl = url
s.PrevSubUrl = ""
s.NextSubUrl = ""
@@ -290,5 +412,5 @@ func (s *Server) HandleNewQuery(w http.ResponseWriter, r *http.Request) {
go s.LoadNext()
go s.LoadPrev()
http.Redirect(w, r, "/current/", http.StatusTemporaryRedirect)
http.Redirect(w, r, "/current/", http.StatusFound)
}

View File

@@ -0,0 +1,20 @@
package server
import (
"net/http"
)
func (s *Server) Auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cookie, _ := r.Cookie("auth")
if r.URL.Path == "/login" || r.URL.Path == "/login/" {
next.ServeHTTP(w, r)
return
}
if s.secret == "" || (cookie != nil && cookie.Value == s.secret) {
next.ServeHTTP(w, r)
} else {
http.Redirect(w, r, "/login", http.StatusFound)
}
})
}

View File

@@ -0,0 +1,62 @@
package server
import "time"
type Options struct {
Port int
Auth Optional[AuthOptions]
Tls Optional[TlsOptions]
UpdateInterval time.Duration
}
type Optional[v any] struct {
Enabled bool
value v
}
func (o *Optional[v]) Get() v {
return o.value
}
func (o *Optional[v]) Set(value v) {
o.value = value
o.Enabled = true
}
func (o *Optional[v]) Apply(apply func(*v)) {
o.Enabled = true
apply(&o.value)
}
type AuthType int
const (
Raw AuthType = iota
File
)
type AuthOptions struct {
// Secret Direct or Path to secret File
Secret string
LoadType AuthType
Secure bool
MaxAge int
}
type TlsOptions struct {
CertPath string
KeyPath string
}
func NewDefaultOptions() Options {
return Options{
Port: 8080,
Auth: Optional[AuthOptions]{
Enabled: false,
},
Tls: Optional[TlsOptions]{
Enabled: false,
},
UpdateInterval: 15 * time.Minute,
}
}

View File

@@ -2,17 +2,22 @@ package server
import (
"bytes"
"crypto/tls"
_ "embed"
"fmt"
"io"
"mangaGetter/internal/database"
"mangaGetter/internal/provider"
"mangaGetter/internal/view"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/pablu23/mangaGetter/internal/database"
"github.com/pablu23/mangaGetter/internal/provider"
"github.com/pablu23/mangaGetter/internal/view"
"github.com/rs/zerolog/log"
)
type Server struct {
@@ -20,7 +25,7 @@ type Server struct {
CurrViewModel *view.ImageViewModel
NextViewModel *view.ImageViewModel
ImageBuffers map[string]*bytes.Buffer
ImageBuffers map[string][]byte
Mutex *sync.Mutex
NextSubUrl string
@@ -33,23 +38,126 @@ type Server struct {
IsLast bool
DbMgr *database.Manager
mux *http.ServeMux
options Options
secret string
}
func New(provider provider.Provider, db *database.Manager) *Server {
func New(provider provider.Provider, db *database.Manager, mux *http.ServeMux, options ...func(*Options)) *Server {
opts := NewDefaultOptions()
for _, opt := range options {
opt(&opts)
}
s := Server{
ImageBuffers: make(map[string]*bytes.Buffer),
ImageBuffers: make(map[string][]byte),
Provider: provider,
DbMgr: db,
Mutex: &sync.Mutex{},
mux: mux,
options: opts,
}
return &s
}
func (s *Server) RegisterRoutes() {
s.mux.HandleFunc("GET /login", s.HandleLogin)
s.mux.HandleFunc("POST /login", s.HandleLoginPost)
s.mux.HandleFunc("/", s.HandleMenu)
s.mux.HandleFunc("/new/", s.HandleNewQuery)
s.mux.HandleFunc("/new/title/{title}/{chapter}", s.HandleNew)
s.mux.HandleFunc("/current/", s.HandleCurrent)
s.mux.HandleFunc("/img/{url}", s.HandleImage)
s.mux.HandleFunc("POST /next", s.HandleNext)
s.mux.HandleFunc("POST /prev", s.HandlePrev)
s.mux.HandleFunc("POST /exit", s.HandleExit)
s.mux.HandleFunc("POST /delete", s.HandleDelete)
s.mux.HandleFunc("/favicon.ico", s.HandleFavicon)
s.mux.HandleFunc("POST /setting/", s.HandleSetting)
s.mux.HandleFunc("GET /setting/set/{setting}/{value}", s.HandleSettingSet)
s.mux.HandleFunc("GET /update", s.HandleUpdate)
s.mux.HandleFunc("POST /disable", s.HandleDisable)
s.mux.HandleFunc("GET /archive", s.HandleArchive)
}
func (s *Server) Start() error {
server := http.Server{
Addr: fmt.Sprintf(":%d", s.options.Port),
Handler: s.mux,
}
s.RegisterRoutes()
s.registerUpdater()
if s.options.Auth.Enabled {
auth := s.options.Auth.Get()
switch auth.LoadType {
case Raw:
s.secret = auth.Secret
case File:
secretBytes, err := os.ReadFile(auth.Secret)
if err != nil {
return err
}
s.secret = string(secretBytes)
}
s.secret = strings.TrimSpace(s.secret)
server.Handler = s.Auth(s.mux)
}
if s.options.Tls.Enabled {
tlsOpts := s.options.Tls.Get()
server.TLSConfig = &tls.Config{
GetCertificate: func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) {
cert, err := tls.LoadX509KeyPair(tlsOpts.CertPath, tlsOpts.KeyPath)
if err != nil {
return nil, err
}
return &cert, err
},
}
log.Info().Int("Port", s.options.Port).Str("Cert", tlsOpts.CertPath).Str("Key", tlsOpts.KeyPath).Msg("Starting server")
return server.ListenAndServeTLS("", "")
} else {
log.Info().Int("Port", s.options.Port).Msg("Starting server")
return server.ListenAndServe()
}
}
func (s *Server) UpdateMangaList() {
var all []*database.Manga
s.DbMgr.Db.Where("enabled = 1").Find(&all)
for _, m := range all {
err, updated := s.UpdateLatestAvailableChapter(m)
if err != nil {
log.Error().Err(err).Str("Manga", m.Title).Msg("Could not update latest available chapters")
}
if updated {
s.DbMgr.Db.Save(m)
}
}
}
func (s *Server) registerUpdater() {
if s.options.UpdateInterval > 0 {
log.Info().Str("Interval", s.options.UpdateInterval.String()).Msg("Registering Updater")
go func(s *Server) {
for {
select {
case <-time.After(s.options.UpdateInterval):
s.UpdateMangaList()
}
}
}(s)
}
}
func (s *Server) LoadNext() {
c, err := s.Provider.GetHtml(s.CurrSubUrl)
if err != nil {
fmt.Println(err)
log.Error().Err(err).Msg("Could not get Html for current chapter")
s.NextSubUrl = ""
s.NextViewModel = nil
return
@@ -57,7 +165,7 @@ func (s *Server) LoadNext() {
next, err := s.Provider.GetNext(c)
if err != nil {
fmt.Println(err)
log.Error().Err(err).Msg("Could not load next chapter")
s.NextSubUrl = ""
s.NextViewModel = nil
return
@@ -65,7 +173,7 @@ func (s *Server) LoadNext() {
html, err := s.Provider.GetHtml(next)
if err != nil {
fmt.Println(err)
log.Error().Err(err).Msg("Could not get Html for next chapter")
s.NextSubUrl = ""
s.NextViewModel = nil
return
@@ -73,7 +181,7 @@ func (s *Server) LoadNext() {
imagesNext, err := s.AppendImagesToBuf(html)
if err != nil {
fmt.Println(err)
log.Error().Err(err).Msg("Could not download images")
s.NextSubUrl = ""
s.NextViewModel = nil
return
@@ -81,6 +189,7 @@ func (s *Server) LoadNext() {
title, chapter, err := s.Provider.GetTitleAndChapter(next)
if err != nil {
log.Warn().Err(err).Str("Url", next).Msg("Could not extract title and chapter")
title = "Unknown"
chapter = "ch_?"
}
@@ -89,27 +198,27 @@ func (s *Server) LoadNext() {
s.NextViewModel = &view.ImageViewModel{Images: imagesNext, Title: full}
s.NextSubUrl = next
fmt.Println("Loaded next")
log.Debug().Msg("Successfully loaded next chapter")
}
func (s *Server) LoadPrev() {
c, err := s.Provider.GetHtml(s.CurrSubUrl)
if err != nil {
fmt.Println(err)
log.Error().Err(err).Msg("Could not get Html for current chapter")
s.PrevSubUrl = ""
s.PrevViewModel = nil
return
}
prev, err := s.Provider.GetPrev(c)
if err != nil {
fmt.Println(err)
log.Error().Err(err).Msg("Could not load prev chapter")
s.PrevSubUrl = ""
s.PrevViewModel = nil
return
}
html, err := s.Provider.GetHtml(prev)
if err != nil {
fmt.Println(err)
log.Error().Err(err).Msg("Could not get Html for prev chapter")
s.PrevSubUrl = ""
s.PrevViewModel = nil
return
@@ -117,7 +226,7 @@ func (s *Server) LoadPrev() {
imagesNext, err := s.AppendImagesToBuf(html)
if err != nil {
fmt.Println(err)
log.Error().Err(err).Msg("Could not download images")
s.PrevSubUrl = ""
s.PrevViewModel = nil
return
@@ -125,6 +234,7 @@ func (s *Server) LoadPrev() {
title, chapter, err := s.Provider.GetTitleAndChapter(prev)
if err != nil {
log.Warn().Err(err).Str("Url", prev).Msg("Could not extract title and chapter")
title = "Unknown"
chapter = "ch_?"
}
@@ -134,19 +244,28 @@ func (s *Server) LoadPrev() {
s.PrevViewModel = &view.ImageViewModel{Images: imagesNext, Title: full}
s.PrevSubUrl = prev
fmt.Println("Loaded prev")
log.Debug().Msg("Successfully loaded prev chapter")
}
func (s *Server) LoadCurr() {
html, err := s.Provider.GetHtml(s.CurrSubUrl)
if err != nil {
panic(err)
log.Error().Err(err).Msg("Could not get Html for current chapter")
s.NextSubUrl = ""
s.PrevSubUrl = ""
s.CurrSubUrl = ""
s.NextViewModel = nil
s.CurrViewModel = nil
s.PrevViewModel = nil
return
}
imagesCurr, err := s.AppendImagesToBuf(html)
title, chapter, err := s.Provider.GetTitleAndChapter(s.CurrSubUrl)
if err != nil {
log.Warn().Err(err).Str("Url", s.CurrSubUrl).Msg("Could not extract title and chapter")
title = "Unknown"
chapter = "ch_?"
}
@@ -154,28 +273,58 @@ func (s *Server) LoadCurr() {
full := strings.Replace(title, "-", " ", -1) + " - " + strings.Replace(chapter, "_", " ", -1)
s.CurrViewModel = &view.ImageViewModel{Images: imagesCurr, Title: full}
fmt.Println("Loaded current")
log.Debug().Msg("Successfully loaded curr chapter")
}
func (s *Server) LoadThumbnail(mangaId int) (path string, err error) {
strId := strconv.Itoa(mangaId)
func (s *Server) UpdateLatestAvailableChapter(manga *database.Manga) (error, bool) {
log.Info().Str("Manga", manga.Title).Msg("Updating Manga")
l, err := s.Provider.GetChapterList("/title/" + strconv.Itoa(manga.Id))
if err != nil {
return err, false
}
le := len(l)
_, c, err := s.Provider.GetTitleAndChapter(l[le-1])
if err != nil {
return err, false
}
chapterNumberStr := strings.Replace(c, "ch_", "", 1)
if manga.LastChapterNum == chapterNumberStr {
return nil, false
} else {
manga.LastChapterNum = chapterNumberStr
return nil, true
}
}
func (s *Server) LoadThumbnail(manga *database.Manga) (path string, updated bool, err error) {
strId := strconv.Itoa(manga.Id)
s.Mutex.Lock()
defer s.Mutex.Unlock()
if s.ImageBuffers[strId] != nil {
return strId, nil
return strId, false, nil
}
url, err := s.Provider.GetThumbnail(strconv.Itoa(mangaId))
if manga.Thumbnail != nil {
s.ImageBuffers[strId] = manga.Thumbnail
return strId, false, nil
}
url, err := s.Provider.GetThumbnail(strId)
if err != nil {
return "", err
return "", false, err
}
ram, err := addFileToRam(url)
if err != nil {
return "", err
return "", false, err
}
manga.Thumbnail = ram
s.ImageBuffers[strId] = ram
return strId, nil
return strId, true, nil
}
func (s *Server) AppendImagesToBuf(html string) ([]view.Image, error) {
@@ -207,7 +356,7 @@ func (s *Server) AppendImagesToBuf(html string) ([]view.Image, error) {
return images, nil
}
func addFileToRam(url string) (*bytes.Buffer, error) {
func addFileToRam(url string) ([]byte, error) {
// Get the data
resp, err := http.Get(url)
if err != nil {
@@ -216,7 +365,7 @@ func addFileToRam(url string) (*bytes.Buffer, error) {
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
fmt.Println(err)
log.Error().Err(err).Msg("Could not close http body")
}
}(resp.Body)
@@ -224,5 +373,5 @@ func addFileToRam(url string) (*bytes.Buffer, error) {
// Write the body to file
_, err = io.Copy(buf, resp.Body)
return buf, err
return buf.Bytes(), err
}

View File

@@ -0,0 +1,80 @@
<!DOCTYPE html>
<!--suppress CssUnusedSymbol -->
<html lang="en">
<head>
<style>
body {
background-color: rgba(10, 11, 15, 255);
margin: 0;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
form {
display: flex;
height: 100%;
flex-direction: column;
align-items: center;
justify-content: center;
}
input {
background-color: rgba(10, 11, 15, 255);
border: 1px solid rgba(104, 85, 224, 1);
border-radius: 4px;
font-weight: 600;
margin: 0;
width: 280px;
height: 30px;
padding: 10px;
}
input:focus {
outline: none;
}
#formcontainer {
border-radius: 1rem;
color: rgb(104, 85, 224);
font-weight: 600;
font-size: 30px;
background-color: rgba(16, 17, 22, 255);
height: 40%;
width: 30%;
box-shadow: 5px 5px 15px 5px rgba(0, 0, 0, 0.34);
}
#loginbutton {
color: rgb(104, 85, 224);
cursor: pointer;
font-weight: 600;
width: 300px;
height: 50px;
transition: 0.4s;
margin-top: 2rem;
}
#loginbutton:hover {
color: white;
box-shadow: 0 0 20px rgba(104, 85, 224, 0.6);
background-color: rgba(104, 85, 224, 1);
}
#passwordinput {
color: white;
}
#passwordinputbox {
display: flex;
flex-direction: column;
}
</style>
</head>
<body>
<div id="formcontainer">
<form method="post" action="/login">
<div id="passwordinputbox">
<label id="passwordlabel"> Password: </label>
<input id="passwordinput" type="password" name="secret" />
</div>
<input id="loginbutton" type="submit" value="Login" />
</form>
</div>
</body>
</html>

View File

@@ -1,160 +1,211 @@
<!DOCTYPE html>
<!--suppress CssUnusedSymbol -->
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Main Menu</title>
<meta charset="UTF-8">
<title>{{if .Archive}}Archive{{else}}Main Menu{{end}}</title>
<style>
body {
padding: 25px;
background-color: white;
color: black;
font-size: 25px;
}
<style>
body {
padding: 25px;
background-color: white;
color: black;
font-size: 25px;
}
.dark-mode {
background-color: #171717;
color: white;
}
.dark {
background-color: #171717;
color: white;
}
.button-36 {
background-image: linear-gradient(92.88deg, #455EB5 9.16%, #5643CC 43.89%, #673FD7 64.72%);
border-radius: 8px;
border-style: none;
box-sizing: border-box;
color: #FFFFFF;
cursor: pointer;
flex-shrink: 0;
font-family: "Inter UI","SF Pro Display",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Open Sans","Helvetica Neue",sans-serif;
font-size: 16px;
font-weight: 500;
height: 4rem;
padding: 0 1.6rem;
text-align: center;
text-shadow: rgba(0, 0, 0, 0.25) 0 3px 8px;
transition: all .5s;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
}
.white {
background-color: white;
color: black;
}
.button-36:hover {
box-shadow: rgba(80, 63, 205, 0.5) 0 1px 30px;
transition-duration: .1s;
}
.button-36 {
background-image: linear-gradient(92.88deg, #455EB5 9.16%, #5643CC 43.89%, #673FD7 64.72%);
border-radius: 8px;
border-style: none;
box-sizing: border-box;
color: #FFFFFF;
cursor: pointer;
flex-shrink: 0;
font-family: "Inter UI", "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
font-size: 16px;
font-weight: 500;
height: 4rem;
padding: 0 1.6rem;
text-align: center;
text-shadow: rgba(0, 0, 0, 0.25) 0 3px 8px;
transition: all .5s;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
}
@media (min-width: 768px) {
.button-36 {
padding: 0 2.6rem;
}
.button-delete{
padding: 0 2.6rem;
}
}
.button-36:hover {
box-shadow: rgba(80, 63, 205, 0.5) 0 1px 30px;
transition-duration: .1s;
}
.button-delete{
background-image: linear-gradient(92.88deg, #f44336 9.16%, #f44336 43.89%, #f44336 64.72%);
border-radius: 8px;
border-style: none;
box-sizing: border-box;
color: #FFFFFF;
cursor: pointer;
flex-shrink: 0;
font-family: "Inter UI","SF Pro Display",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Open Sans","Helvetica Neue",sans-serif;
font-size: 16px;
font-weight: 500;
height: 4rem;
padding: 0 1.6rem;
text-align: center;
text-shadow: rgba(0, 0, 0, 0.25) 0 3px 8px;
transition: all .5s;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
}
@media (min-width: 768px) {
.button-36 {
padding: 0 2.6rem;
}
.button-delete:hover {
box-shadow: rgba(244, 67, 54, 0.5) 0 1px 30px;
transition-duration: .1s;
}
.button-delete {
padding: 0 2.6rem;
}
}
.table-left{
text-align: left;
}
.button-delete {
background-image: linear-gradient(92.88deg, #f44336 9.16%, #f44336 43.89%, #f44336 64.72%);
border-radius: 8px;
border-style: none;
box-sizing: border-box;
color: #FFFFFF;
cursor: pointer;
flex-shrink: 0;
font-family: "Inter UI", "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
font-size: 16px;
font-weight: 500;
height: 4rem;
padding: 0 1.6rem;
text-align: center;
text-shadow: rgba(0, 0, 0, 0.25) 0 3px 8px;
transition: all .5s;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
}
.thumbnail{
border: 1px solid #ddd; /* Gray border */
border-radius: 4px; /* Rounded border */
padding: 5px; /* Some padding */
width: 150px; /* Set a small width */
}
.button-delete:hover {
box-shadow: rgba(244, 67, 54, 0.5) 0 1px 30px;
transition-duration: .1s;
}
.thumbnail:hover{
box-shadow: 0 0 2px 1px rgba(0, 140, 186, 0.5);
}
.table-left {
text-align: left;
}
.table {
width: 100%;
}
.thumbnail {
border: 1px solid #ddd;
/* Gray border */
border-radius: 4px;
/* Rounded border */
padding: 5px;
/* Some padding */
width: 150px;
/* Set a small width */
}
td{
text-align: center;
}
.thumbnail:hover {
box-shadow: 0 0 2px 1px rgba(0, 140, 186, 0.5);
}
.table {
width: 100%;
}
</style>
<script>
function myFunction() {
var element = document.body;
element.classList.toggle("dark-mode");
}
</script>
td {
text-align: center;
}
label {
display: flex;
align-content: center;
justify-content: center;
margin: 0 auto;
}
select {
width: 10em;
margin-bottom: 10px;
margin-top: 10px;
}
</style>
</head>
<body>
<form method="post" action="/new/">
<label>
New Sub Url
<input type="text" name="subUrl">
</label>
<input type="submit" value="Open" class="button-36">
</form>
<button onclick="myFunction()">Toggle dark mode</button>
<table class="table">
<tr>
<th>Thumbnail</th>
<th class="table-left">Title</th>
<th>Current Chapter</th>
<th>Last Accessed</th>
<th>Link</th>
<th>Delete</th>
</tr>
{{range .Mangas}}
<tr>
<td>
<a target="_blank" href="/img/{{.ThumbnailUrl}}">
<img class="thumbnail" src="/img/{{.ThumbnailUrl}}" alt="img_{{.ThumbnailUrl}}"/>
</a>
</td>
<td class="table-left">{{.Title}}</td>
<td>{{.Number}}</td>
<td>{{.LastTime}}</td>
<td>
<a href="/new/{{.Url}}}">
<button class="button-36">
To chapter
</button>
</a>
</td>
<td>
<form method="post" action="/delete">
<input type="hidden" name="mangaId" value="{{.ID}}">
<input type="submit" class="button-delete" value="Delete">
</form>
</td>
</tr>
{{end}}
</table>
<body class='{{(index .Settings "theme").Value}}'>
<form method="post" action="/new/">
<label>
New Sub Url
<input type="text" name="subUrl">
</label>
<input type="submit" value="Open" class="button-36">
</form>
<a href='{{if .Archive}}/{{else}}/archive{{end}}'>
<button class="button-36">
{{if .Archive}}
To Main Menu
{{else}}
To Archive
{{end}}
</button>
</a>
{{if not .Archive}}
<a href="/update">
<button class="button-36">
Update Chapters
</button>
</a>
{{end}}
<form method="post" action="/setting/">
<label for="theme">Theme</label>
<select onchange="this.form.submit()" id="theme" name="theme">
<option {{if eq (index .Settings "theme" ).Value "white" }} selected {{end}} value="white">White</option>
<option {{if eq (index .Settings "theme" ).Value "dark" }} selected {{end}} value="dark">Dark</option>
</select>
<input type="hidden" name="setting" value="theme">
</form>
<table class="table">
<tr>
<th>Thumbnail</th>
<th class="table-left"><a href="setting/set/order/title">Title</a></th>
<th><a href="setting/set/order/chapter">Current Chapter</a></th>
<th><a href="setting/set/order/last">Last Accessed</a></th>
<th>Link</th>
<th>Disable/Enable</th>
<th>Delete</th>
</tr>
{{range .Mangas}}
<tr>
<td>
<a target="_blank" href="/img/{{.ThumbnailUrl}}">
<img class="thumbnail" src="/img/{{.ThumbnailUrl}}" alt="img_{{.ThumbnailUrl}}" />
</a>
</td>
<td class="table-left">{{.Title}}</td>
<td>{{.Number}} / {{.LastNumber}}</td>
<td>{{.LastTime}}</td>
<td>
<a href="/new/{{.Url}}">
<button class="button-36">
To chapter
</button>
</a>
</td>
<td>
<form method="post" action="/disable">
<input type="hidden" name="mangaId" value="{{.ID}}">
<input type="submit" class="button-delete" value="{{if .Enabled}}Disable{{else}}Enable{{end}}">
</form>
</td>
<td>
<form method="post" action="/delete">
<input type="hidden" name="mangaId" value="{{.ID}}">
<input type="submit" class="button-delete" value="Delete">
</form>
</td>
</tr>
{{end}}
</table>
</body>
</html>
</html>

View File

@@ -31,6 +31,12 @@
vertical-align: middle;
}
@media (max-width: 500px) {
.scroll-container img {
width: 100%;
}
}
/*
* I have no clue what css is, jesus christ ...
*/
@@ -45,6 +51,18 @@
display: flex;
}
.fixed-button {
position: fixed;
bottom: 80px;
right: 15px;
margin-bottom: 40px;
z-index: 999;
}
.text{
color: white;
}
.button-36 {
background-image: linear-gradient(92.88deg, #455EB5 9.16%, #5643CC 43.89%, #673FD7 64.72%);
border-radius: 8px;
@@ -79,13 +97,17 @@
</style>
</head>
<body>
<div class="center">
<h1 class="center text">{{.Title}}</h1>
<div class="center" id="top">
<form method="post" action="/prev">
<input type="submit" name="Prev" value="Prev" class="button-36">
<input type="submit" name="Exit" value="Exit" class="button-36" formaction="/exit">
<input type="submit" name="Next" value="Next" class="button-36" formaction="/next">
</form>
</div>
<button class="fixed-button">
<a href="#top">TOP</a>
</button>
<div class="scroll-container">
{{range .Images}}
<img src="/img/{{.Path}}" alt="img_{{.Index}}"/>

View File

@@ -14,12 +14,17 @@ var menu string
//go:embed Views/viewer.gohtml
var viewer string
//go:embed Views/login.gohtml
var login string
func GetViewTemplate(view View) (*template.Template, error) {
switch view {
case Menu:
return template.New("menu").Parse(menu)
case Viewer:
return template.New("viewer").Parse(viewer)
case Login:
return template.New("login").Parse(login)
}
return nil, errors.New("invalid view")
}

View File

@@ -13,6 +13,8 @@ func GetViewTemplate(view View) (*template.Template, error) {
path = "internal/view/Views/menu.gohtml"
case Viewer:
path = "internal/view/Views/viewer.gohtml"
case Login:
path = "internal/view/Views/login.gohtml"
}
return template.ParseFiles(path)
}

View File

@@ -1,5 +1,7 @@
package view
import "github.com/pablu23/mangaGetter/internal/database"
type Image struct {
Path string
Index int
@@ -13,12 +15,16 @@ type ImageViewModel struct {
type MangaViewModel struct {
ID int
Title string
Number int
Number string
LastNumber string
LastTime string
Url string
ThumbnailUrl string
Enabled bool
}
type MenuViewModel struct {
Mangas []MangaViewModel
Archive bool
Settings map[string]database.Setting
Mangas []MangaViewModel
}

View File

@@ -5,4 +5,5 @@ type View int
const (
Menu View = iota
Viewer View = iota
Login View = iota
)

180
main.go Normal file
View File

@@ -0,0 +1,180 @@
package main
import (
"flag"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"os/signal"
"runtime"
"time"
"github.com/pablu23/mangaGetter/internal/database"
"github.com/pablu23/mangaGetter/internal/provider"
"github.com/pablu23/mangaGetter/internal/server"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"gopkg.in/natefinch/lumberjack.v2"
)
var (
secretFlag = flag.String("secret", "", "Secret to use for Auth")
authFlag = flag.Bool("auth", false, "Use Auth, does not need to be set if secret or secret-path is set")
secretFilePathFlag = flag.String("secret-path", "", "Path to file with ONLY secret in it")
portFlag = flag.Int("port", 80, "The port on which to host")
serverFlag = flag.Bool("server", false, "If false dont open Browser with Address")
databaseFlag = flag.String("database", "", "Path to sqlite.db file")
certFlag = flag.String("cert", "", "Path to cert file, has to be used in conjunction with key")
keyFlag = flag.String("key", "", "Path to key file, has to be used in conjunction with cert")
updateIntervalFlag = flag.String("update", "0h", "Interval to update Mangas")
debugFlag = flag.Bool("debug", false, "Activate debug Logs")
prettyLogsFlag = flag.Bool("pretty", false, "Pretty pring Logs")
logPathFlag = flag.String("log", "", "Path to logfile, stderr if default")
maxAgeFlag = flag.Int("age", 3600, "Max age for login Session")
secureFlag = flag.Bool("secure", false, "Cookie secure?")
)
func main() {
flag.Parse()
setupLogging()
filePath := setupDb()
db := database.NewDatabase(filePath, true, *debugFlag)
err := db.Open()
if err != nil {
log.Fatal().Err(err).Str("Path", filePath).Msg("Could not open Database")
}
mux := http.NewServeMux()
s := server.New(&provider.Bato{}, &db, mux, func(o *server.Options) {
authOptions := setupAuth()
o.Port = *portFlag
if *secretFlag != "" || *secretFilePathFlag != "" || *authFlag {
o.Auth.Set(authOptions)
}
interval, err := time.ParseDuration(*updateIntervalFlag)
if err != nil {
log.Fatal().Err(err).Str("Interval", *updateIntervalFlag).Msg("Could not parse interval")
}
o.UpdateInterval = interval
if *certFlag != "" && *keyFlag != "" {
o.Tls.Apply(func(to *server.TlsOptions) {
to.CertPath = *certFlag
to.KeyPath = *keyFlag
})
}
})
setupClient()
setupClose(&db)
err = s.Start()
if err != nil {
log.Fatal().Err(err).Msg("Could not start server")
}
}
func setupAuth() server.AuthOptions {
var authOptions server.AuthOptions
if *secretFlag != "" {
authOptions.LoadType = server.Raw
authOptions.Secret = *secretFlag
} else if *secretFilePathFlag != "" {
authOptions.LoadType = server.File
authOptions.Secret = *secretFilePathFlag
} else if *authFlag {
path, err := getSecretPath()
if err != nil {
log.Fatal().Err(err).Msg("Secret file could not be found")
}
authOptions.Secret = path
authOptions.LoadType = server.File
}
authOptions.MaxAge = *maxAgeFlag
authOptions.Secure = *secureFlag
return authOptions
}
func setupClient() {
if !*serverFlag {
go func() {
time.Sleep(300 * time.Millisecond)
err := open(fmt.Sprintf("http://localhost:%d", *portFlag))
if err != nil {
log.Error().Err(err).Msg("Could not open Browser")
}
}()
}
}
func setupClose(db *database.Manager) {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
for range c {
Close(db)
}
}()
}
func setupDb() string {
if *databaseFlag != "" {
return *databaseFlag
} else {
return getDbPath()
}
}
func setupLogging() {
zerolog.SetGlobalLevel(zerolog.InfoLevel)
if *prettyLogsFlag {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
}
if !*debugFlag {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
if *logPathFlag != "" {
var console io.Writer = os.Stderr
if *prettyLogsFlag {
console = zerolog.ConsoleWriter{Out: os.Stderr}
}
log.Logger = log.Output(zerolog.MultiLevelWriter(console, &lumberjack.Logger{
Filename: *logPathFlag,
MaxAge: 14,
MaxBackups: 10,
}))
}
}
func open(url string) error {
var cmd string
var args []string
switch runtime.GOOS {
case "windows":
cmd = "cmd"
args = []string{"/c", "start"}
case "darwin":
cmd = "open"
default: // "linux", "freebsd", "openbsd", "netbsd"
cmd = "xdg-open"
}
args = append(args, url)
return exec.Command(cmd, args...).Start()
}
func Close(db *database.Manager) {
log.Debug().Msg("Closing Database")
err := db.Close()
if err != nil {
log.Error().Err(err).Msg("Could not close Database")
return
}
os.Exit(0)
}

64
release.go Normal file
View File

@@ -0,0 +1,64 @@
//go:build !Develop
package main
import (
"os"
"path/filepath"
)
func getSecretPath() (string, error) {
dir, err := os.UserCacheDir()
if err != nil {
return "", err
}
dirPath := filepath.Join(dir, "MangaGetter")
filePath := filepath.Join(dirPath, "secret.secret")
return filePath, nil
}
func getSecret() (string, error) {
dir, err := os.UserCacheDir()
if err != nil {
return "", err
}
dirPath := filepath.Join(dir, "MangaGetter")
filePath := filepath.Join(dirPath, "secret.secret")
buf, err := os.ReadFile(filePath)
if err != nil {
return "", err
}
return string(buf), nil
}
func getDbPath() string {
dir, err := os.UserCacheDir()
if err != nil {
panic(err)
}
dirPath := filepath.Join(dir, "MangaGetter")
filePath := filepath.Join(dirPath, "db.sqlite")
if _, err := os.Stat(dirPath); os.IsNotExist(err) {
err = os.Mkdir(dirPath, os.ModePerm)
if err != nil {
panic(err)
}
}
if _, err := os.Stat(filePath); os.IsNotExist(err) {
f, err := os.Create(filePath)
if err != nil {
panic(err)
}
err = f.Close()
if err != nil {
panic(err)
}
}
return filePath
}

2
test.log Normal file
View File

@@ -0,0 +1,2 @@
{"level":"info","Port":8080,"time":"2024-05-31T00:22:54+02:00","message":"Starting server"}
{"level":"debug","time":"2024-05-31T00:22:58+02:00","message":"Closing Database"}