1 Commits

30 changed files with 776 additions and 1336 deletions

9
.gitignore vendored
View File

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

View File

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

View File

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

121
cmd/mangaGetter/main.go Normal file
View File

@@ -0,0 +1,121 @@
package main
import (
"fmt"
"mangaGetter/internal/database"
"mangaGetter/internal/server"
"net/http"
"os"
"os/exec"
"os/signal"
"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("db.sqlite", true)
err := db.Open()
if err != nil {
fmt.Println(err)
return
}
s := server.New(&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("POST /new/", s.HandleNewQuery)
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)
}

View File

@@ -1,15 +0,0 @@
//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 github.com/pablu23/mangaGetter module mangaGetter
go 1.22 go 1.22
@@ -7,18 +7,4 @@ require (
golang.org/x/text v0.14.0 golang.org/x/text v0.14.0
) )
require ( require github.com/google/uuid v1.6.0 // indirect
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
)

33
go.sum
View File

@@ -1,35 +1,6 @@
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 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 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 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

@@ -1,21 +1,11 @@
package database package database
type Chapter struct { type Chapter struct {
Id int `gorm:"primary_key;AUTO_INCREMENT"` Id int
Url string Manga *Manga
Name string InternalIdentifier string
Number string Url string
TimeStampUnix int64 Name string
MangaId int Number int
} TimeStampUnix int64
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

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

View File

@@ -1,75 +1,206 @@
package database package database
import ( import (
"bytes"
"database/sql"
_ "embed" _ "embed"
"fmt"
"mangaGetter/internal/provider"
"sync"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
) )
type Manager struct { type Manager struct {
ConnectionString string ConnectionString string
Db *gorm.DB db *sql.DB
CreateIfNotExists bool
ActivateGormLogger bool Rw *sync.Mutex
Mangas map[int]*Manga
Chapters map[int]*Chapter
CreateIfNotExists bool
} }
func NewDatabase(connectionString string, createIfNotExists bool, activateGormLogger bool) Manager { func NewDatabase(connectionString string, createIfNotExists bool) Manager {
return Manager{ return Manager{
ConnectionString: connectionString, ConnectionString: connectionString,
Db: nil, Rw: &sync.Mutex{},
CreateIfNotExists: createIfNotExists, Mangas: make(map[int]*Manga),
ActivateGormLogger: activateGormLogger, Chapters: make(map[int]*Chapter),
CreateIfNotExists: createIfNotExists,
} }
} }
func (dbMgr *Manager) Open() error { func (dbMgr *Manager) Open() error {
var db *gorm.DB db, err := sql.Open("sqlite3", dbMgr.ConnectionString)
var err error if err != nil {
if dbMgr.ActivateGormLogger { return err
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 { if dbMgr.CreateIfNotExists {
err = dbMgr.createDatabaseIfNotExists() err = dbMgr.createDatabaseIfNotExists()
if err != nil { if err != nil {
return err return err
} }
} }
err = dbMgr.load()
return err return err
} }
func (dbMgr *Manager) Close() error { func (dbMgr *Manager) Close() error {
sql, err := dbMgr.Db.DB() err := dbMgr.db.Close()
if err != nil { if err != nil {
return err return err
} }
err = sql.Close()
if err != nil { dbMgr.Mangas = nil
return err dbMgr.Chapters = nil
} dbMgr.db = nil
dbMgr.Db = nil
return nil return nil
} }
func (dbMgr *Manager) Delete(mangaId int) { func (dbMgr *Manager) Delete(mangaId int) error {
dbMgr.Db.Delete(&Manga{}, mangaId) 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)
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)
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
}
//go:embed createDb.sql
var createSql string
func (dbMgr *Manager) createDatabaseIfNotExists() error { func (dbMgr *Manager) createDatabaseIfNotExists() error {
err := dbMgr.Db.AutoMigrate(&Manga{}, &Chapter{}, &Setting{}) _, err := dbMgr.db.Exec(createSql)
return err 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 ID, Provider, Title, TimeStampUnixEpoch, Thumbnail, Rating FROM Manga")
if err != nil {
return err
}
for rows.Next() {
manga := Manga{}
var thumbnail []byte
var providerId int
if err = rows.Scan(&manga.Id, &providerId, &manga.Title, &manga.TimeStampUnix, &thumbnail, &manga.Rating); err != nil {
return err
}
manga.Thumbnail = bytes.NewBuffer(thumbnail)
manga.Provider = provider.GetProviderByType(provider.ProviderType(providerId))
dbMgr.Mangas[manga.Id] = &manga
}
rows, err = db.Query("SELECT ID, MangaID, Url, Name, Number, TimeStampUnixEpoch, InternalIdentifier 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, &chapter.InternalIdentifier); 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

@@ -1,49 +1,18 @@
package database package database
import (
"bytes"
"mangaGetter/internal/provider"
)
type Manga struct { type Manga struct {
Id int `gorm:"primary_key;AUTO_INCREMENT"` Id int
Title string Provider provider.Provider
TimeStampUnix int64 Rating int
Thumbnail []byte Title string
LastChapterNum string TimeStampUnix int64
Chapters []Chapter Thumbnail *bytes.Buffer
Enabled bool
//`gorm:"foreignkey:MangaID"` // Not in DB
} LatestChapter *Chapter
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

@@ -1,27 +0,0 @@
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

@@ -0,0 +1,100 @@
package provider
import (
"errors"
"fmt"
"io"
"net/http"
"regexp"
"strconv"
)
type Asura struct{}
func (a Asura) GetImageList(html string) (imageUrls []string, err error) {
reg, err := regexp.Compile(`<img decoding="async" class="ts-main-image " src="(.*?)"`)
if err != nil {
return nil, err
}
m := reg.FindAllStringSubmatch(html, -1)
l := len(m)
result := make([]string, l)
for i, match := range m {
result[i] = match[1]
}
return result, nil
}
func (a Asura) GetHtml(url string) (html string, err error) {
resp, err := http.Get(url)
// TODO: Testing for above 300 is dirty
if err != nil && resp.StatusCode > 300 {
return "", errors.New("could not get html")
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
fmt.Printf("Could not close body because: %v\n", err)
}
}(resp.Body)
all, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
h := string(all)
return h, nil
}
func (a Asura) GetNext(html string) (url string, err error) {
//TODO implement me
return "#/next/", nil
}
func (a Asura) GetPrev(html string) (url string, err error) {
//TODO implement me
return "#/prev/", nil
}
func (a Asura) GetTitleAndChapter(url string) (title string, chapter string, err error) {
//TODO implement me
reg, err := regexp.Compile(`\d*-(.*?)-(\d*)/`)
if err != nil {
return "", "", err
}
matches := reg.FindAllStringSubmatch(url, -1)
if len(matches) <= 0 {
return "", "", errors.New("no title or chapter found")
}
return matches[0][1], matches[0][2], nil
}
func (a Asura) GetTitleIdAndChapterId(url string) (titleId int, chapterId int, err error) {
//TODO implement me
reg, err := regexp.Compile(`(\d*)-.*?-(\d*)/`)
if err != nil {
return 0, 0, err
}
matches := reg.FindAllStringSubmatch(url, -1)
if len(matches) <= 0 {
return 0, 0, errors.New("no title or chapter found")
}
t, err := strconv.Atoi(matches[0][1])
if err != nil {
return 0, 0, err
}
c, err := strconv.Atoi(matches[0][2])
return t, c, nil
}
func (a Asura) GetThumbnail(mangaId string) (thumbnailUrl string, err error) {
//TODO implement me
panic("implement me")
}

View File

@@ -0,0 +1,18 @@
package provider
type ProviderType int
const (
BatoId ProviderType = iota
AsuraId ProviderType = iota
)
func GetProviderByType(typeId ProviderType) Provider {
switch typeId {
case BatoId:
return &Bato{}
case AsuraId:
return &Asura{}
}
return nil
}

View File

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

View File

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

View File

@@ -1,194 +1,69 @@
package server package server
import ( import (
"bytes"
"cmp" "cmp"
_ "embed" _ "embed"
"errors"
"fmt" "fmt"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"html/template" "html/template"
"mangaGetter/internal/database"
"mangaGetter/internal/view"
"net/http" "net/http"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
"time" "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.CurrSubUrl = url
s.PrevSubUrl = ""
s.NextSubUrl = ""
s.LoadCurr()
go s.LoadNext()
go s.LoadPrev()
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) { 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)) tmpl := template.Must(view.GetViewTemplate(view.Menu))
l := len(mangas) 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)
mangaViewModels := make([]view.MangaViewModel, l) mangaViewModels := make([]view.MangaViewModel, l)
counter := 0 counter := 0
//TODO: Change all this to be more performant for _, manga := range all {
for _, manga := range mangas {
title := cases.Title(language.English, cases.Compact).String(strings.Replace(manga.Title, "-", " ", -1)) title := cases.Title(language.English, cases.Compact).String(strings.Replace(manga.Title, "-", " ", -1))
thumbnail, updated, err := s.LoadThumbnail(manga) thumbnail, err := s.LoadThumbnail(manga.Id)
//TODO: Add default picture instead of not showing Manga at all
if err != nil { if err != nil {
continue continue
} }
if updated { manga.Thumbnail = s.ImageBuffers[thumbnail]
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{ mangaViewModels[counter] = view.MangaViewModel{
ID: manga.Id, ID: manga.Id,
Title: title, Title: title,
Number: latestChapter.Number, Number: manga.LatestChapter.Number,
LastNumber: manga.LastChapterNum,
// I Hate this time Format... 15 = hh, 04 = mm, 02 = DD, 01 = MM, 06 == YY // 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)"), LastTime: time.Unix(manga.TimeStampUnix, 0).Format("15:04 (02-01-06)"),
Url: latestChapter.Url, Url: manga.LatestChapter.Url,
ThumbnailUrl: thumbnail, ThumbnailUrl: thumbnail,
Enabled: manga.Enabled,
} }
counter++ counter++
} }
order, ok := settings["order"] slices.SortStableFunc(mangaViewModels, func(a, b view.MangaViewModel) int {
if !ok || order.Value == "title" { return cmp.Compare(a.Title, b.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{ menuViewModel := view.MenuViewModel{
Settings: settings, Mangas: mangaViewModels,
Mangas: mangaViewModels,
Archive: archive,
} }
err := tmpl.Execute(w, menuViewModel) err := tmpl.Execute(w, menuViewModel)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Could not template Menu") fmt.Println(err)
} }
} }
@@ -196,86 +71,93 @@ func (s *Server) HandleDelete(w http.ResponseWriter, r *http.Request) {
mangaStr := r.PostFormValue("mangaId") mangaStr := r.PostFormValue("mangaId")
if mangaStr == "" { if mangaStr == "" {
http.Redirect(w, r, "/", http.StatusFound) http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
return return
} }
mangaId, err := strconv.Atoi(mangaStr) mangaId, err := strconv.Atoi(mangaStr)
if err != nil { if err != nil {
log.Error().Err(err).Str("Id", mangaStr).Msg("Could not convert id to int") fmt.Println(err)
http.Redirect(w, r, "/", http.StatusFound) http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
return return
} }
s.DbMgr.Delete(mangaId) err = s.DbMgr.Delete(mangaId)
if err != nil {
fmt.Println(err)
}
http.Redirect(w, r, "/", http.StatusFound) http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
} }
func (s *Server) HandleExit(w http.ResponseWriter, r *http.Request) { func (s *Server) HandleExit(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusFound) err := s.DbMgr.Save()
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 { if err != nil {
log.Error().Err(err).Str("subUrl", s.CurrSubUrl).Msg("Could not get TitleId and ChapterId") fmt.Println(err)
http.Redirect(w, r, "/", http.StatusFound)
return return
} }
title, chapterName, err := s.Provider.GetTitleAndChapter(s.CurrSubUrl) 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)
if err != nil { if err != nil {
log.Warn().Err(err).Str("subUrl", s.CurrSubUrl).Msg("Could not get Title and Chapter") fmt.Println(err)
}
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 { } else {
manga.TimeStampUnix = time.Now().Unix() 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()
}
var chapter database.Chapter if s.DbMgr.Chapters[chapterId] == nil {
result = s.DbMgr.Db.First(&chapter, chapterId) chapterNumberStr := strings.Replace(chapter, "ch_", "", 1)
if result.Error != nil && errors.Is(result.Error, gorm.ErrRecordNotFound) { number, err := strconv.Atoi(chapterNumberStr)
chapterNumberStr := strings.Replace(chapterName, "ch_", "", 1) if err != nil {
chapter = database.NewChapter(chapterId, mangaId, s.CurrSubUrl, chapterName, chapterNumberStr, time.Now().Unix()) fmt.Println(err)
} else { number = 0
chapter.TimeStampUnix = time.Now().Unix() }
}
s.DbMgr.Db.Save(&manga) s.DbMgr.Chapters[chapterId] = &database.Chapter{
s.DbMgr.Db.Save(&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]
}
}
err = tmpl.Execute(w, s.CurrViewModel) err = tmpl.Execute(w, s.CurrViewModel)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Could not template Current") fmt.Println(err)
} }
} }
@@ -285,30 +167,32 @@ func (s *Server) HandleImage(w http.ResponseWriter, r *http.Request) {
defer s.Mutex.Unlock() defer s.Mutex.Unlock()
buf := s.ImageBuffers[u] buf := s.ImageBuffers[u]
if buf == nil { if buf == nil {
log.Warn().Str("url", u).Msg("Image not found") fmt.Printf("url: %s is nil\n", u)
w.WriteHeader(http.StatusNotFound) w.WriteHeader(400)
return return
} }
w.Header().Set("Content-Type", "image/webp") w.Header().Set("Content-Type", "image/webp")
_, err := w.Write(buf) _, err := w.Write(buf.Bytes())
if err != nil { if err != nil {
log.Error().Err(err).Msg("Could not write image") fmt.Println(err)
} }
} }
//go:embed favicon.ico //go:embed favicon.ico
var ico []byte var ico []byte
func (s *Server) HandleFavicon(w http.ResponseWriter, _ *http.Request) { func (s *Server) HandleFavicon(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "image/webp") w.Header().Set("Content-Type", "image/webp")
_, err := w.Write(ico) _, err := w.Write(ico)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Could not write favicon") fmt.Println(err)
} }
} }
func (s *Server) HandleNext(w http.ResponseWriter, r *http.Request) { func (s *Server) HandleNext(w http.ResponseWriter, r *http.Request) {
fmt.Println("Received Next")
if s.PrevViewModel != nil { if s.PrevViewModel != nil {
go func(viewModel view.ImageViewModel, s *Server) { go func(viewModel view.ImageViewModel, s *Server) {
s.Mutex.Lock() s.Mutex.Lock()
@@ -316,12 +200,16 @@ func (s *Server) HandleNext(w http.ResponseWriter, r *http.Request) {
delete(s.ImageBuffers, img.Path) delete(s.ImageBuffers, img.Path)
} }
s.Mutex.Unlock() s.Mutex.Unlock()
log.Debug().Msg("Cleaned imagebuffer") fmt.Println("Cleaned out of scope Last")
}(*s.PrevViewModel, s) }(*s.PrevViewModel, s)
} }
if s.NextViewModel == nil || s.NextSubUrl == "" { if s.NextViewModel == nil || s.NextSubUrl == "" {
http.Redirect(w, r, "/", http.StatusFound) http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
err := s.DbMgr.Save()
if err != nil {
fmt.Println(err)
}
return return
} }
@@ -332,10 +220,10 @@ func (s *Server) HandleNext(w http.ResponseWriter, r *http.Request) {
go s.LoadNext() go s.LoadNext()
http.Redirect(w, r, "/current/", http.StatusFound) http.Redirect(w, r, "/current/", http.StatusTemporaryRedirect)
} }
func (s *Server) HandlePrev(w http.ResponseWriter, r *http.Request) { func (s *Server) HandlePrev(w http.ResponseWriter, r *http.Request) {
fmt.Println("Received Prev")
if s.NextViewModel != nil { if s.NextViewModel != nil {
go func(viewModel view.ImageViewModel, s *Server) { go func(viewModel view.ImageViewModel, s *Server) {
s.Mutex.Lock() s.Mutex.Lock()
@@ -343,12 +231,16 @@ func (s *Server) HandlePrev(w http.ResponseWriter, r *http.Request) {
delete(s.ImageBuffers, img.Path) delete(s.ImageBuffers, img.Path)
} }
s.Mutex.Unlock() s.Mutex.Unlock()
log.Debug().Msg("Cleaned imagebuffer") fmt.Println("Cleaned out of scope Last")
}(*s.NextViewModel, s) }(*s.NextViewModel, s)
} }
if s.PrevViewModel == nil || s.PrevSubUrl == "" { if s.PrevViewModel == nil || s.PrevSubUrl == "" {
http.Redirect(w, r, "/", http.StatusFound) http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
err := s.DbMgr.Save()
if err != nil {
fmt.Println(err)
}
return return
} }
@@ -359,51 +251,15 @@ func (s *Server) HandlePrev(w http.ResponseWriter, r *http.Request) {
go s.LoadPrev() go s.LoadPrev()
http.Redirect(w, r, "/current/", http.StatusFound) http.Redirect(w, r, "/current/", http.StatusTemporaryRedirect)
}
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) { func (s *Server) HandleNewQuery(w http.ResponseWriter, r *http.Request) {
sub := r.PostFormValue("subUrl") sub := r.PostFormValue("subUrl")
sub = s.Provider.CleanUrlToSub(sub) s.Mutex.Lock()
url := fmt.Sprintf("/title/%s", sub) s.ImageBuffers = make(map[string]*bytes.Buffer)
s.Mutex.Unlock()
s.CurrSubUrl = url s.CurrSubUrl = url
s.PrevSubUrl = "" s.PrevSubUrl = ""
s.NextSubUrl = "" s.NextSubUrl = ""
@@ -412,5 +268,5 @@ func (s *Server) HandleNewQuery(w http.ResponseWriter, r *http.Request) {
go s.LoadNext() go s.LoadNext()
go s.LoadPrev() go s.LoadPrev()
http.Redirect(w, r, "/current/", http.StatusFound) http.Redirect(w, r, "/current/", http.StatusTemporaryRedirect)
} }

View File

@@ -1,20 +0,0 @@
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

@@ -1,62 +0,0 @@
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,178 +2,67 @@ package server
import ( import (
"bytes" "bytes"
"crypto/tls"
_ "embed" _ "embed"
"fmt" "fmt"
"github.com/google/uuid"
"io" "io"
"mangaGetter/internal/database"
"mangaGetter/internal/view"
"net/http" "net/http"
"os"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"sync" "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 { type Server struct {
ContextManga *database.Manga
PrevViewModel *view.ImageViewModel PrevViewModel *view.ImageViewModel
CurrViewModel *view.ImageViewModel CurrViewModel *view.ImageViewModel
NextViewModel *view.ImageViewModel NextViewModel *view.ImageViewModel
ImageBuffers map[string][]byte ImageBuffers map[string]*bytes.Buffer
Mutex *sync.Mutex Mutex *sync.Mutex
NextSubUrl string NextSubUrl string
CurrSubUrl string CurrSubUrl string
PrevSubUrl string PrevSubUrl string
Provider provider.Provider
IsFirst bool IsFirst bool
IsLast bool IsLast bool
DbMgr *database.Manager DbMgr *database.Manager
mux *http.ServeMux
options Options
secret string
} }
func New(provider provider.Provider, db *database.Manager, mux *http.ServeMux, options ...func(*Options)) *Server { func New(db *database.Manager) *Server {
opts := NewDefaultOptions()
for _, opt := range options {
opt(&opts)
}
s := Server{ s := Server{
ImageBuffers: make(map[string][]byte), ImageBuffers: make(map[string]*bytes.Buffer),
Provider: provider,
DbMgr: db, DbMgr: db,
Mutex: &sync.Mutex{}, Mutex: &sync.Mutex{},
mux: mux,
options: opts,
} }
return &s 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() { func (s *Server) LoadNext() {
c, err := s.Provider.GetHtml(s.CurrSubUrl) c, err := s.ContextManga.Provider.GetHtml(s.CurrSubUrl)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Could not get Html for current chapter") fmt.Println(err)
s.NextSubUrl = "" s.NextSubUrl = ""
s.NextViewModel = nil s.NextViewModel = nil
return return
} }
next, err := s.Provider.GetNext(c) next, err := s.ContextManga.Provider.GetNext(c)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Could not load next chapter") fmt.Println(err)
s.NextSubUrl = "" s.NextSubUrl = ""
s.NextViewModel = nil s.NextViewModel = nil
return return
} }
html, err := s.Provider.GetHtml(next) html, err := s.ContextManga.Provider.GetHtml(next)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Could not get Html for next chapter") fmt.Println(err)
s.NextSubUrl = "" s.NextSubUrl = ""
s.NextViewModel = nil s.NextViewModel = nil
return return
@@ -181,15 +70,14 @@ func (s *Server) LoadNext() {
imagesNext, err := s.AppendImagesToBuf(html) imagesNext, err := s.AppendImagesToBuf(html)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Could not download images") fmt.Println(err)
s.NextSubUrl = "" s.NextSubUrl = ""
s.NextViewModel = nil s.NextViewModel = nil
return return
} }
title, chapter, err := s.Provider.GetTitleAndChapter(next) title, chapter, err := s.ContextManga.Provider.GetTitleAndChapter(next)
if err != nil { if err != nil {
log.Warn().Err(err).Str("Url", next).Msg("Could not extract title and chapter")
title = "Unknown" title = "Unknown"
chapter = "ch_?" chapter = "ch_?"
} }
@@ -198,27 +86,27 @@ func (s *Server) LoadNext() {
s.NextViewModel = &view.ImageViewModel{Images: imagesNext, Title: full} s.NextViewModel = &view.ImageViewModel{Images: imagesNext, Title: full}
s.NextSubUrl = next s.NextSubUrl = next
log.Debug().Msg("Successfully loaded next chapter") fmt.Println("Loaded next")
} }
func (s *Server) LoadPrev() { func (s *Server) LoadPrev() {
c, err := s.Provider.GetHtml(s.CurrSubUrl) c, err := s.ContextManga.Provider.GetHtml(s.CurrSubUrl)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Could not get Html for current chapter") fmt.Println(err)
s.PrevSubUrl = "" s.PrevSubUrl = ""
s.PrevViewModel = nil s.PrevViewModel = nil
return return
} }
prev, err := s.Provider.GetPrev(c) prev, err := s.ContextManga.Provider.GetPrev(c)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Could not load prev chapter") fmt.Println(err)
s.PrevSubUrl = "" s.PrevSubUrl = ""
s.PrevViewModel = nil s.PrevViewModel = nil
return return
} }
html, err := s.Provider.GetHtml(prev) html, err := s.ContextManga.Provider.GetHtml(prev)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Could not get Html for prev chapter") fmt.Println(err)
s.PrevSubUrl = "" s.PrevSubUrl = ""
s.PrevViewModel = nil s.PrevViewModel = nil
return return
@@ -226,15 +114,14 @@ func (s *Server) LoadPrev() {
imagesNext, err := s.AppendImagesToBuf(html) imagesNext, err := s.AppendImagesToBuf(html)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Could not download images") fmt.Println(err)
s.PrevSubUrl = "" s.PrevSubUrl = ""
s.PrevViewModel = nil s.PrevViewModel = nil
return return
} }
title, chapter, err := s.Provider.GetTitleAndChapter(prev) title, chapter, err := s.ContextManga.Provider.GetTitleAndChapter(prev)
if err != nil { if err != nil {
log.Warn().Err(err).Str("Url", prev).Msg("Could not extract title and chapter")
title = "Unknown" title = "Unknown"
chapter = "ch_?" chapter = "ch_?"
} }
@@ -244,28 +131,19 @@ func (s *Server) LoadPrev() {
s.PrevViewModel = &view.ImageViewModel{Images: imagesNext, Title: full} s.PrevViewModel = &view.ImageViewModel{Images: imagesNext, Title: full}
s.PrevSubUrl = prev s.PrevSubUrl = prev
log.Debug().Msg("Successfully loaded prev chapter") fmt.Println("Loaded prev")
} }
func (s *Server) LoadCurr() { func (s *Server) LoadCurr() {
html, err := s.Provider.GetHtml(s.CurrSubUrl) html, err := s.ContextManga.Provider.GetHtml(s.CurrSubUrl)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Could not get Html for current chapter") panic(err)
s.NextSubUrl = ""
s.PrevSubUrl = ""
s.CurrSubUrl = ""
s.NextViewModel = nil
s.CurrViewModel = nil
s.PrevViewModel = nil
return
} }
imagesCurr, err := s.AppendImagesToBuf(html) imagesCurr, err := s.AppendImagesToBuf(html)
title, chapter, err := s.Provider.GetTitleAndChapter(s.CurrSubUrl) title, chapter, err := s.ContextManga.Provider.GetTitleAndChapter(s.CurrSubUrl)
if err != nil { if err != nil {
log.Warn().Err(err).Str("Url", s.CurrSubUrl).Msg("Could not extract title and chapter")
title = "Unknown" title = "Unknown"
chapter = "ch_?" chapter = "ch_?"
} }
@@ -273,62 +151,32 @@ func (s *Server) LoadCurr() {
full := strings.Replace(title, "-", " ", -1) + " - " + strings.Replace(chapter, "_", " ", -1) full := strings.Replace(title, "-", " ", -1) + " - " + strings.Replace(chapter, "_", " ", -1)
s.CurrViewModel = &view.ImageViewModel{Images: imagesCurr, Title: full} s.CurrViewModel = &view.ImageViewModel{Images: imagesCurr, Title: full}
log.Debug().Msg("Successfully loaded curr chapter") fmt.Println("Loaded current")
} }
func (s *Server) UpdateLatestAvailableChapter(manga *database.Manga) (error, bool) { func (s *Server) LoadThumbnail(mangaId int) (path string, err error) {
log.Info().Str("Manga", manga.Title).Msg("Updating Manga") strId := strconv.Itoa(mangaId)
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() s.Mutex.Lock()
defer s.Mutex.Unlock() defer s.Mutex.Unlock()
if s.ImageBuffers[strId] != nil { if s.ImageBuffers[strId] != nil {
return strId, false, nil return strId, nil
} }
if manga.Thumbnail != nil { url, err := s.ContextManga.Provider.GetThumbnail(strconv.Itoa(mangaId))
s.ImageBuffers[strId] = manga.Thumbnail
return strId, false, nil
}
url, err := s.Provider.GetThumbnail(strId)
if err != nil { if err != nil {
return "", false, err return "", err
} }
ram, err := addFileToRam(url) ram, err := addFileToRam(url)
if err != nil { if err != nil {
return "", false, err return "", err
} }
manga.Thumbnail = ram
s.ImageBuffers[strId] = ram s.ImageBuffers[strId] = ram
return strId, true, nil return strId, nil
} }
func (s *Server) AppendImagesToBuf(html string) ([]view.Image, error) { func (s *Server) AppendImagesToBuf(html string) ([]view.Image, error) {
imgList, err := s.Provider.GetImageList(html) imgList, err := s.ContextManga.Provider.GetImageList(html)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -343,11 +191,11 @@ func (s *Server) AppendImagesToBuf(html string) ([]view.Image, error) {
if err != nil { if err != nil {
panic(err) panic(err)
} }
name := filepath.Base(url) g := uuid.New()
s.Mutex.Lock() s.Mutex.Lock()
s.ImageBuffers[name] = buf s.ImageBuffers[g.String()] = buf
s.Mutex.Unlock() s.Mutex.Unlock()
images[i] = view.Image{Path: name, Index: i} images[i] = view.Image{Path: g.String(), Index: i}
wg.Done() wg.Done()
}(i, url, &wg) }(i, url, &wg)
} }
@@ -356,7 +204,7 @@ func (s *Server) AppendImagesToBuf(html string) ([]view.Image, error) {
return images, nil return images, nil
} }
func addFileToRam(url string) ([]byte, error) { func addFileToRam(url string) (*bytes.Buffer, error) {
// Get the data // Get the data
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil { if err != nil {
@@ -365,7 +213,7 @@ func addFileToRam(url string) ([]byte, error) {
defer func(Body io.ReadCloser) { defer func(Body io.ReadCloser) {
err := Body.Close() err := Body.Close()
if err != nil { if err != nil {
log.Error().Err(err).Msg("Could not close http body") fmt.Println(err)
} }
}(resp.Body) }(resp.Body)
@@ -373,5 +221,5 @@ func addFileToRam(url string) ([]byte, error) {
// Write the body to file // Write the body to file
_, err = io.Copy(buf, resp.Body) _, err = io.Copy(buf, resp.Body)
return buf.Bytes(), err return buf, err
} }

View File

@@ -1,80 +0,0 @@
<!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,211 +1,160 @@
<!DOCTYPE html> <!DOCTYPE html>
<!--suppress CssUnusedSymbol -->
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{{if .Archive}}Archive{{else}}Main Menu{{end}}</title> <title>Main Menu</title>
<style> <style>
body { body {
padding: 25px; padding: 25px;
background-color: white; background-color: white;
color: black; color: black;
font-size: 25px; font-size: 25px;
} }
.dark { .dark-mode {
background-color: #171717; background-color: #171717;
color: white; color: white;
} }
.white { .button-36 {
background-color: white; background-image: linear-gradient(92.88deg, #455EB5 9.16%, #5643CC 43.89%, #673FD7 64.72%);
color: black; 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;
}
.button-36 { .button-36:hover {
background-image: linear-gradient(92.88deg, #455EB5 9.16%, #5643CC 43.89%, #673FD7 64.72%); box-shadow: rgba(80, 63, 205, 0.5) 0 1px 30px;
border-radius: 8px; transition-duration: .1s;
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;
}
.button-36:hover { @media (min-width: 768px) {
box-shadow: rgba(80, 63, 205, 0.5) 0 1px 30px; .button-36 {
transition-duration: .1s; padding: 0 2.6rem;
} }
.button-delete{
padding: 0 2.6rem;
}
}
@media (min-width: 768px) { .button-delete{
.button-36 { background-image: linear-gradient(92.88deg, #f44336 9.16%, #f44336 43.89%, #f44336 64.72%);
padding: 0 2.6rem; 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;
}
.button-delete { .button-delete:hover {
padding: 0 2.6rem; box-shadow: rgba(244, 67, 54, 0.5) 0 1px 30px;
} transition-duration: .1s;
} }
.button-delete { .table-left{
background-image: linear-gradient(92.88deg, #f44336 9.16%, #f44336 43.89%, #f44336 64.72%); text-align: left;
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;
}
.button-delete:hover { .thumbnail{
box-shadow: rgba(244, 67, 54, 0.5) 0 1px 30px; border: 1px solid #ddd; /* Gray border */
transition-duration: .1s; border-radius: 4px; /* Rounded border */
} padding: 5px; /* Some padding */
width: 150px; /* Set a small width */
}
.table-left { .thumbnail:hover{
text-align: left; box-shadow: 0 0 2px 1px rgba(0, 140, 186, 0.5);
} }
.thumbnail { .table {
border: 1px solid #ddd; width: 100%;
/* Gray border */ }
border-radius: 4px;
/* Rounded border */
padding: 5px;
/* Some padding */
width: 150px;
/* Set a small width */
}
.thumbnail:hover { td{
box-shadow: 0 0 2px 1px rgba(0, 140, 186, 0.5); text-align: center;
} }
.table {
width: 100%;
}
td { </style>
text-align: center; <script>
} function myFunction() {
var element = document.body;
element.classList.toggle("dark-mode");
}
</script>
label {
display: flex;
align-content: center;
justify-content: center;
margin: 0 auto;
}
select {
width: 10em;
margin-bottom: 10px;
margin-top: 10px;
}
</style>
</head> </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>
<body class='{{(index .Settings "theme").Value}}'> <table class="table">
<form method="post" action="/new/"> <tr>
<label> <th>Thumbnail</th>
New Sub Url <th class="table-left">Title</th>
<input type="text" name="subUrl"> <th>Current Chapter</th>
</label> <th>Last Accessed</th>
<input type="submit" value="Open" class="button-36"> <th>Link</th>
</form> <th>Delete</th>
</tr>
<a href='{{if .Archive}}/{{else}}/archive{{end}}'> {{range .Mangas}}
<button class="button-36"> <tr>
{{if .Archive}} <td>
To Main Menu <a target="_blank" href="/img/{{.ThumbnailUrl}}">
{{else}} <img class="thumbnail" src="/img/{{.ThumbnailUrl}}" alt="img_{{.ThumbnailUrl}}"/>
To Archive </a>
{{end}} </td>
</button> <td class="table-left">{{.Title}}</td>
</a> <td>{{.Number}}</td>
<td>{{.LastTime}}</td>
{{if not .Archive}} <td>
<a href="/update"> <a href="/new/{{.Url}}}">
<button class="button-36"> <button class="button-36">
Update Chapters To chapter
</button> </button>
</a> </a>
{{end}} </td>
<td>
<form method="post" action="/setting/"> <form method="post" action="/delete">
<label for="theme">Theme</label> <input type="hidden" name="mangaId" value="{{.ID}}">
<select onchange="this.form.submit()" id="theme" name="theme"> <input type="submit" class="button-delete" value="Delete">
<option {{if eq (index .Settings "theme" ).Value "white" }} selected {{end}} value="white">White</option> </form>
<option {{if eq (index .Settings "theme" ).Value "dark" }} selected {{end}} value="dark">Dark</option> </td>
</select> </tr>
<input type="hidden" name="setting" value="theme"> {{end}}
</form> </table>
<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> </body>
</html> </html>

View File

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

View File

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

View File

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

View File

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

View File

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

180
main.go
View File

@@ -1,180 +0,0 @@
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)
}

View File

@@ -1,64 +0,0 @@
//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
}

View File

@@ -1,2 +0,0 @@
{"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"}