23 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
26 changed files with 970 additions and 618 deletions

4
.gitignore vendored
View File

@@ -6,3 +6,7 @@ h.html
*.bak *.bak
/bin /bin
*.exe *.exe
*secret*
mangaGetter
*.crt
*.key

View File

@@ -1,6 +1,5 @@
run: develop run: develop
bin/develop bin/develop --secret test --server --port 8181 --database db.sqlite --debug --pretty
develop: develop:
go build -tags Develop -o bin/develop go build -tags Develop -o bin/develop
release: release:

View File

@@ -2,7 +2,13 @@
package main package main
const port = 8080 func getSecretPath() (string, error) {
return "", nil
}
func getSecret() (string, error) {
return "test", nil
}
func getDbPath() string { func getDbPath() string {
return "db.sqlite" return "db.sqlite"

9
go.mod
View File

@@ -7,9 +7,18 @@ require (
golang.org/x/text v0.14.0 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 ( require (
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // 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/driver/sqlite v1.5.5
gorm.io/gorm v1.25.10 gorm.io/gorm v1.25.10
) )

23
go.sum
View File

@@ -1,11 +1,34 @@
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 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 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 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 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 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E=
gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE= 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 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=

View File

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

View File

@@ -2,30 +2,46 @@ package database
import ( import (
_ "embed" _ "embed"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"gorm.io/driver/sqlite" "gorm.io/driver/sqlite"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/logger"
) )
type Manager struct { type Manager struct {
ConnectionString string ConnectionString string
Db *gorm.DB Db *gorm.DB
CreateIfNotExists bool CreateIfNotExists bool
ActivateGormLogger bool
} }
func NewDatabase(connectionString string, createIfNotExists bool) Manager { func NewDatabase(connectionString string, createIfNotExists bool, activateGormLogger bool) Manager {
return Manager{ return Manager{
ConnectionString: connectionString, ConnectionString: connectionString,
Db: nil, Db: nil,
CreateIfNotExists: createIfNotExists, CreateIfNotExists: createIfNotExists,
ActivateGormLogger: activateGormLogger,
} }
} }
func (dbMgr *Manager) Open() error { func (dbMgr *Manager) Open() error {
db, err := gorm.Open(sqlite.Open(dbMgr.ConnectionString), &gorm.Config{}) var db *gorm.DB
if err != nil { var err error
return err 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 { if dbMgr.CreateIfNotExists {
err = dbMgr.createDatabaseIfNotExists() err = dbMgr.createDatabaseIfNotExists()
@@ -54,6 +70,6 @@ func (dbMgr *Manager) Delete(mangaId int) {
} }
func (dbMgr *Manager) createDatabaseIfNotExists() error { func (dbMgr *Manager) createDatabaseIfNotExists() error {
err := dbMgr.Db.AutoMigrate(&MangaDefinition{}, &User{}, &Manga{}, &Chapter{}, &Setting{}) err := dbMgr.Db.AutoMigrate(&Manga{}, &Chapter{}, &Setting{})
return err return err
} }

View File

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

View File

@@ -1,17 +0,0 @@
package database
type User struct {
Id int `gorm:"primary_key;AUTO_INCREMENT"`
DisplayName string
LoginName string
PwdHash []byte
Salt []byte
}
// type UserManga struct {
// Id int `gorm:"primary_key;AUTO_INCREMENT"`
// DisplayName string
// Manga Manga
// User User
// // Chapters []Chapter `gorm:"ForeignKey:ChapterId,UserId;References:Id,UserId"`
// }

View File

@@ -7,10 +7,19 @@ 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 {
@@ -49,7 +58,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 {
fmt.Printf("Could not close body because: %v\n", err) log.Error().Err(err).Msg("Could not close http body")
} }
}(resp.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) { 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="(.*?)["']`)
match := reg.FindStringSubmatch(html) if err != nil {
return "", err
}
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
} }

View File

@@ -1,6 +1,7 @@
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)

View File

@@ -14,70 +14,52 @@ import (
"github.com/pablu23/mangaGetter/internal/database" "github.com/pablu23/mangaGetter/internal/database"
"github.com/pablu23/mangaGetter/internal/view" "github.com/pablu23/mangaGetter/internal/view"
"github.com/rs/zerolog/log"
"golang.org/x/text/cases" "golang.org/x/text/cases"
"golang.org/x/text/language" "golang.org/x/text/language"
"gorm.io/gorm" "gorm.io/gorm"
) )
func (s *Server) getSessionFromCookie(w http.ResponseWriter, r *http.Request) (*UserSession, error) { func (s *Server) HandleDisable(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session") id := r.PostFormValue("mangaId")
if err != nil { var manga database.Manga
switch { s.DbMgr.Db.Where("id = ?", id).First(&manga)
case errors.Is(err, http.ErrNoCookie):
// http.Error(w, "cookie not found", http.StatusBadRequest) if manga.Enabled {
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect) http.Redirect(w, r, "/", http.StatusFound)
default: } else {
fmt.Println(err) http.Redirect(w, r, "/archive", http.StatusFound)
http.Error(w, "server error", http.StatusInternalServerError)
}
} }
session, ok := s.Sessions[cookie.Value]
if !ok { manga.Enabled = !manga.Enabled
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect) s.DbMgr.Db.Save(&manga)
return nil, errors.New("Unknown Session")
}
return session, err
} }
func (s *Server) HandleRegister(w http.ResponseWriter, r *http.Request) { func (s *Server) HandleUpdate(w http.ResponseWriter, r *http.Request) {
s.Mutex.Lock() s.UpdateMangaList()
defer s.Mutex.Unlock() http.Redirect(w, r, "/", http.StatusFound)
}
admin := database.User{ func (s *Server) HandleLoginPost(w http.ResponseWriter, r *http.Request) {
Id: 1, if s.options.Auth.Enabled {
DisplayName: "admin", auth := s.options.Auth.Get()
LoginName: "admin", 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,
})
} }
s.DbMgr.Db.Create(&admin) http.Redirect(w, r, "/", http.StatusFound)
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
} }
func (s *Server) HandleLogin(w http.ResponseWriter, r *http.Request) { func (s *Server) HandleLogin(w http.ResponseWriter, r *http.Request) {
s.Mutex.Lock() tmpl := template.Must(view.GetViewTemplate(view.Login))
defer s.Mutex.Unlock() tmpl.Execute(w, nil)
// Login
s.Sessions["abcd"] = &UserSession{
User: database.User{
Id: 1,
DisplayName: "admin",
LoginName: "admin",
},
}
cookie := http.Cookie{
Name: "session",
Value: "abcd",
Path: "/",
MaxAge: 3600,
Secure: true,
HttpOnly: false,
SameSite: http.SameSiteLaxMode,
}
http.SetCookie(w, &cookie)
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
} }
func (s *Server) HandleNew(w http.ResponseWriter, r *http.Request) { func (s *Server) HandleNew(w http.ResponseWriter, r *http.Request) {
@@ -86,36 +68,20 @@ func (s *Server) HandleNew(w http.ResponseWriter, r *http.Request) {
url := fmt.Sprintf("/title/%s/%s", title, chapter) url := fmt.Sprintf("/title/%s/%s", title, chapter)
session, err := s.getSessionFromCookie(w, r) s.CurrSubUrl = url
if err != nil { s.PrevSubUrl = ""
return s.NextSubUrl = ""
} s.LoadCurr()
session.CurrSubUrl = url go s.LoadNext()
session.PrevSubUrl = "" go s.LoadPrev()
session.NextSubUrl = ""
s.LoadCurr(session)
go s.LoadNext(session) http.Redirect(w, r, "/current/", http.StatusFound)
go s.LoadPrev(session)
http.Redirect(w, r, "/current/", http.StatusTemporaryRedirect)
} }
func (s *Server) HandleMenu(w http.ResponseWriter, r *http.Request) { func (s *Server) HandleArchive(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(view.GetViewTemplate(view.Menu))
session, err := s.getSessionFromCookie(w, r)
if err != nil {
return
}
var all []*database.Manga var all []*database.Manga
_ = s.DbMgr.Db.Preload("Chapters").Where("user_id = ?", session.User.Id).Find(&all) _ = s.DbMgr.Db.Preload("Chapters").Where("enabled = 0").Find(&all)
l := len(all)
mangaViewModels := make([]view.MangaViewModel, l)
counter := 0
n := time.Now().UnixNano()
var tmp []database.Setting var tmp []database.Setting
s.DbMgr.Db.Find(&tmp) s.DbMgr.Db.Find(&tmp)
@@ -124,14 +90,33 @@ func (s *Server) HandleMenu(w http.ResponseWriter, r *http.Request) {
settings[m.Name] = m settings[m.Name] = m
} }
var thumbNs int64 = 0 s.ViewMenu(w, all, settings, true)
var titNs int64 = 0 }
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))
l := len(mangas)
mangaViewModels := make([]view.MangaViewModel, l)
counter := 0
//TODO: Change all this to be more performant //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.Definition.Title, "-", " ", -1)) title := cases.Title(language.English, cases.Compact).String(strings.Replace(manga.Title, "-", " ", -1))
t1 := time.Now().UnixNano()
thumbnail, updated, err := s.LoadThumbnail(manga) thumbnail, updated, err := s.LoadThumbnail(manga)
//TODO: Add default picture instead of not showing Manga at all //TODO: Add default picture instead of not showing Manga at all
@@ -141,54 +126,37 @@ func (s *Server) HandleMenu(w http.ResponseWriter, r *http.Request) {
if updated { if updated {
s.DbMgr.Db.Save(manga) s.DbMgr.Db.Save(manga)
} }
t2 := time.Now().UnixNano()
thumbNs += t2 - t1
t1 = time.Now().UnixNano()
// This is very slow // This is very slow
// TODO: put this into own Method // TODO: put this into own Method
if manga.Definition.LastChapterNum == "" { if manga.LastChapterNum == "" {
err, updated := s.UpdateLatestAvailableChapter(manga) err, updated := s.UpdateLatestAvailableChapter(manga)
if err != nil { if err != nil {
fmt.Println(err) log.Error().Err(err).Msg("Could not update latest available chapters")
} }
if updated { if updated {
s.DbMgr.Db.Save(manga.Definition) s.DbMgr.Db.Save(manga)
} }
} }
t2 = time.Now().UnixNano()
titNs += t2 - t1
latestChapter, ok := manga.GetLatestChapter() latestChapter, ok := manga.GetLatestChapter()
if !ok { if !ok {
continue continue
} }
mangaViewModels[counter] = view.MangaViewModel{ mangaViewModels[counter] = view.MangaViewModel{
ID: manga.Definition.Id, ID: manga.Id,
Title: title, Title: title,
Number: latestChapter.Number, Number: latestChapter.Number,
LastNumber: manga.Definition.LastChapterNum, 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: latestChapter.Url,
ThumbnailUrl: thumbnail, ThumbnailUrl: thumbnail,
Enabled: manga.Enabled,
} }
counter++ counter++
} }
fmt.Printf("Loading Thumbnails took %d ms\n", (thumbNs)/1000000)
fmt.Printf("Loading latest Chapters took %d ms\n", (titNs)/1000000)
nex := time.Now().UnixNano()
fmt.Printf("Creating Viewmodels took %d ms\n", (nex-n)/1000000)
n = time.Now().UnixNano()
order, ok := settings["order"] order, ok := settings["order"]
if !ok || order.Value == "title" { if !ok || order.Value == "title" {
slices.SortStableFunc(mangaViewModels, func(a, b view.MangaViewModel) int { slices.SortStableFunc(mangaViewModels, func(a, b view.MangaViewModel) int {
@@ -212,17 +180,15 @@ func (s *Server) HandleMenu(w http.ResponseWriter, r *http.Request) {
}) })
} }
nex = time.Now().UnixNano()
fmt.Printf("Sorting took %d ms\n", (nex-n)/1000000)
menuViewModel := view.MenuViewModel{ menuViewModel := view.MenuViewModel{
Settings: settings, Settings: settings,
Mangas: mangaViewModels, Mangas: mangaViewModels,
Archive: archive,
} }
err = tmpl.Execute(w, menuViewModel) err := tmpl.Execute(w, menuViewModel)
if err != nil { if err != nil {
fmt.Println(err) log.Error().Err(err).Msg("Could not template Menu")
} }
} }
@@ -230,116 +196,104 @@ 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.StatusTemporaryRedirect) http.Redirect(w, r, "/", http.StatusFound)
return return
} }
mangaId, err := strconv.Atoi(mangaStr) mangaId, err := strconv.Atoi(mangaStr)
if err != nil { if err != nil {
fmt.Println(err) log.Error().Err(err).Str("Id", mangaStr).Msg("Could not convert id to int")
http.Redirect(w, r, "/", http.StatusTemporaryRedirect) http.Redirect(w, r, "/", http.StatusFound)
return return
} }
s.DbMgr.Delete(mangaId) 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) { func (s *Server) HandleExit(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusTemporaryRedirect) http.Redirect(w, r, "/", http.StatusFound)
// session, err := s.getSessionFromCookie(w, r)
// if err != nil {
// return
// }
go func() { go func() {
// session.Mutex.Lock() s.Mutex.Lock()
// if session.PrevViewModel != nil { if s.PrevViewModel != nil {
// for _, img := range session.PrevViewModel.Images { for _, img := range s.PrevViewModel.Images {
// delete(s.ImageBuffers, img.Path) delete(s.ImageBuffers, img.Path)
// } }
// } }
// if session.CurrViewModel != nil { if s.CurrViewModel != nil {
//
// for _, img := range session.CurrViewModel.Images { for _, img := range s.CurrViewModel.Images {
// delete(s.ImageBuffers, img.Path) delete(s.ImageBuffers, img.Path)
// } }
// } }
// if session.NextViewModel != nil { if s.NextViewModel != nil {
//
// for _, img := range session.NextViewModel.Images { for _, img := range s.NextViewModel.Images {
// delete(s.ImageBuffers, img.Path) delete(s.ImageBuffers, img.Path)
// } }
// } }
// session.Mutex.Unlock() s.Mutex.Unlock()
fmt.Println("Cleaned last Manga") log.Info().Msg("Cleaned up images")
}() }()
} }
func (s *Server) HandleCurrent(w http.ResponseWriter, r *http.Request) { func (s *Server) HandleCurrent(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(view.GetViewTemplate(view.Viewer)) tmpl := template.Must(view.GetViewTemplate(view.Viewer))
session, err := s.getSessionFromCookie(w, r) 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")
http.Redirect(w, r, "/", http.StatusFound)
return return
} }
mangaId, chapterId, err := s.Provider.GetTitleIdAndChapterId(session.CurrSubUrl)
if err != nil {
fmt.Println(err)
}
title, chapterName, err := s.Provider.GetTitleAndChapter(session.CurrSubUrl) title, chapterName, err := s.Provider.GetTitleAndChapter(s.CurrSubUrl)
if err != nil { if err != nil {
fmt.Println(err) log.Warn().Err(err).Str("subUrl", s.CurrSubUrl).Msg("Could not get Title and Chapter")
}
var mangaDef database.MangaDefinition
result := s.DbMgr.Db.First(&mangaDef, mangaId)
if result.Error != nil && errors.Is(result.Error, gorm.ErrRecordNotFound) {
mangaDef = database.NewMangaDefinition(mangaId, title)
} }
var manga database.Manga var manga database.Manga
result = s.DbMgr.Db.Where("user_id = ?", session.User.Id).First(&manga, "manga_definition_id = ?", mangaId) result := s.DbMgr.Db.First(&manga, mangaId)
if result.Error != nil && errors.Is(result.Error, gorm.ErrRecordNotFound) { if result.Error != nil && errors.Is(result.Error, gorm.ErrRecordNotFound) {
manga = database.NewManga(mangaDef, session.User, time.Now().Unix()) manga = database.NewManga(mangaId, title, time.Now().Unix())
} else {
manga.TimeStampUnix = time.Now().Unix()
} }
var chapter database.Chapter var chapter database.Chapter
result = s.DbMgr.Db.Where("user_id = ?", session.User.Id).First(&chapter, "chapter_id = ?", chapterId) result = s.DbMgr.Db.First(&chapter, chapterId)
if result.Error != nil && errors.Is(result.Error, gorm.ErrRecordNotFound) { if result.Error != nil && errors.Is(result.Error, gorm.ErrRecordNotFound) {
chapterNumberStr := strings.Replace(chapterName, "ch_", "", 1) chapterNumberStr := strings.Replace(chapterName, "ch_", "", 1)
chapter = database.NewChapter(chapterId, mangaId, session.User.Id, session.CurrSubUrl, chapterName, chapterNumberStr, time.Now().Unix()) chapter = database.NewChapter(chapterId, mangaId, s.CurrSubUrl, chapterName, chapterNumberStr, time.Now().Unix())
} else { } else {
chapter.TimeStampUnix = time.Now().Unix() chapter.TimeStampUnix = time.Now().Unix()
} }
s.DbMgr.Db.Save(&mangaDef)
s.DbMgr.Db.Save(&manga) s.DbMgr.Db.Save(&manga)
s.DbMgr.Db.Save(&chapter) s.DbMgr.Db.Save(&chapter)
err = tmpl.Execute(w, session.CurrViewModel) err = tmpl.Execute(w, s.CurrViewModel)
if err != nil { if err != nil {
fmt.Println(err) log.Error().Err(err).Msg("Could not template Current")
} }
} }
func (s *Server) HandleImage(w http.ResponseWriter, r *http.Request) { func (s *Server) HandleImage(w http.ResponseWriter, r *http.Request) {
u := r.PathValue("url") u := r.PathValue("url")
s.Mutex.RLock() s.Mutex.Lock()
defer s.Mutex.RUnlock() defer s.Mutex.Unlock()
buf := s.ImageBuffers[u] buf := s.ImageBuffers[u]
if buf == nil { if buf == nil {
fmt.Printf("url: %s is nil\n", u) log.Warn().Str("url", u).Msg("Image not found")
w.WriteHeader(400) w.WriteHeader(http.StatusNotFound)
return return
} }
w.Header().Set("Content-Type", "image/webp") w.Header().Set("Content-Type", "image/webp")
_, err := w.Write(buf) _, err := w.Write(buf)
if err != nil { if err != nil {
fmt.Println(err) log.Error().Err(err).Msg("Could not write image")
} }
} }
@@ -350,74 +304,62 @@ func (s *Server) HandleFavicon(w http.ResponseWriter, _ *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 {
fmt.Println(err) log.Error().Err(err).Msg("Could not write favicon")
} }
} }
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 {
session, err := s.getSessionFromCookie(w, r)
if err != nil {
return
}
if session.PrevViewModel != nil {
go func(viewModel view.ImageViewModel, s *Server) { go func(viewModel view.ImageViewModel, s *Server) {
s.Mutex.Lock() s.Mutex.Lock()
for _, img := range viewModel.Images { for _, img := range viewModel.Images {
delete(s.ImageBuffers, img.Path) delete(s.ImageBuffers, img.Path)
} }
s.Mutex.Unlock() s.Mutex.Unlock()
fmt.Println("Cleaned out of scope Last") log.Debug().Msg("Cleaned imagebuffer")
}(*session.PrevViewModel, s) }(*s.PrevViewModel, s)
} }
if session.NextViewModel == nil || session.NextSubUrl == "" { if s.NextViewModel == nil || s.NextSubUrl == "" {
http.Redirect(w, r, "/", http.StatusTemporaryRedirect) http.Redirect(w, r, "/", http.StatusFound)
return return
} }
session.PrevViewModel = session.CurrViewModel s.PrevViewModel = s.CurrViewModel
session.CurrViewModel = session.NextViewModel s.CurrViewModel = s.NextViewModel
session.PrevSubUrl = session.CurrSubUrl s.PrevSubUrl = s.CurrSubUrl
session.CurrSubUrl = session.NextSubUrl s.CurrSubUrl = s.NextSubUrl
go s.LoadNext(session) 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")
session, err := s.getSessionFromCookie(w, r) func (s *Server) HandlePrev(w http.ResponseWriter, r *http.Request) {
if err != nil { if s.NextViewModel != nil {
return
}
if session.NextViewModel != nil {
go func(viewModel view.ImageViewModel, s *Server) { go func(viewModel view.ImageViewModel, s *Server) {
s.Mutex.Lock() s.Mutex.Lock()
for _, img := range viewModel.Images { for _, img := range viewModel.Images {
delete(s.ImageBuffers, img.Path) delete(s.ImageBuffers, img.Path)
} }
s.Mutex.Unlock() s.Mutex.Unlock()
fmt.Println("Cleaned out of scope Last") log.Debug().Msg("Cleaned imagebuffer")
}(*session.NextViewModel, s) }(*s.NextViewModel, s)
} }
if session.PrevViewModel == nil || session.PrevSubUrl == "" { if s.PrevViewModel == nil || s.PrevSubUrl == "" {
http.Redirect(w, r, "/", http.StatusTemporaryRedirect) http.Redirect(w, r, "/", http.StatusFound)
return return
} }
session.NextViewModel = session.CurrViewModel s.NextViewModel = s.CurrViewModel
session.CurrViewModel = session.PrevViewModel s.CurrViewModel = s.PrevViewModel
session.NextSubUrl = session.CurrSubUrl s.NextSubUrl = s.CurrSubUrl
session.CurrSubUrl = session.PrevSubUrl s.CurrSubUrl = s.PrevSubUrl
go s.LoadPrev(session) 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) { func (s *Server) HandleSettingSet(w http.ResponseWriter, r *http.Request) {
@@ -434,7 +376,7 @@ func (s *Server) HandleSettingSet(w http.ResponseWriter, r *http.Request) {
s.DbMgr.Db.Model(&setting).Update("value", settingValue) s.DbMgr.Db.Model(&setting).Update("value", settingValue)
} }
http.Redirect(w, r, "/", http.StatusTemporaryRedirect) http.Redirect(w, r, "/", http.StatusFound)
} }
func (s *Server) HandleSetting(w http.ResponseWriter, r *http.Request) { func (s *Server) HandleSetting(w http.ResponseWriter, r *http.Request) {
@@ -447,29 +389,28 @@ func (s *Server) HandleSetting(w http.ResponseWriter, r *http.Request) {
if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) { if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) {
set := database.NewSetting(settingName, settingValue) set := database.NewSetting(settingName, settingValue)
s.DbMgr.Db.Save(&set) s.DbMgr.Db.Save(&set)
} else if res.Error != nil {
log.Error().Err(res.Error).Send()
} else { } else {
s.DbMgr.Db.Model(&setting).Update("value", settingValue) s.DbMgr.Db.Model(&setting).Update("value", settingValue)
} }
http.Redirect(w, r, "/", http.StatusTemporaryRedirect) 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)
url := fmt.Sprintf("/title/%s", sub) url := fmt.Sprintf("/title/%s", sub)
session, err := s.getSessionFromCookie(w, r) s.CurrSubUrl = url
if err != nil { s.PrevSubUrl = ""
return s.NextSubUrl = ""
} s.LoadCurr()
session.CurrSubUrl = url
session.PrevSubUrl = ""
session.NextSubUrl = ""
s.LoadCurr(session)
go s.LoadNext(session) go s.LoadNext()
go s.LoadPrev(session) 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,218 +2,284 @@ package server
import ( import (
"bytes" "bytes"
"crypto/tls"
_ "embed" _ "embed"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time"
"github.com/pablu23/mangaGetter/internal/database" "github.com/pablu23/mangaGetter/internal/database"
"github.com/pablu23/mangaGetter/internal/provider" "github.com/pablu23/mangaGetter/internal/provider"
"github.com/pablu23/mangaGetter/internal/view" "github.com/pablu23/mangaGetter/internal/view"
"github.com/rs/zerolog/log"
) )
type Server struct { type Server struct {
ImageBuffers map[string][]byte
Provider provider.Provider
DbMgr *database.Manager
Mutex *sync.RWMutex
Sessions map[string]*UserSession
}
type UserSession struct {
User database.User
// Mutex *sync.Mutex
PrevSubUrl string
CurrSubUrl string
NextSubUrl string
PrevViewModel *view.ImageViewModel PrevViewModel *view.ImageViewModel
CurrViewModel *view.ImageViewModel CurrViewModel *view.ImageViewModel
NextViewModel *view.ImageViewModel NextViewModel *view.ImageViewModel
ImageBuffers map[string][]byte
Mutex *sync.Mutex
NextSubUrl string
CurrSubUrl string
PrevSubUrl string
Provider provider.Provider
IsFirst bool
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{ s := Server{
ImageBuffers: make(map[string][]byte), ImageBuffers: make(map[string][]byte),
Sessions: make(map[string]*UserSession),
Provider: provider, Provider: provider,
DbMgr: db, DbMgr: db,
Mutex: &sync.RWMutex{}, Mutex: &sync.Mutex{},
mux: mux,
options: opts,
} }
return &s return &s
} }
func (s *Server) Start(port int) error { func (s *Server) RegisterRoutes() {
http.HandleFunc("/register", s.HandleRegister) s.mux.HandleFunc("GET /login", s.HandleLogin)
http.HandleFunc("/login", s.HandleLogin) s.mux.HandleFunc("POST /login", s.HandleLoginPost)
http.HandleFunc("/", s.HandleMenu) s.mux.HandleFunc("/", s.HandleMenu)
http.HandleFunc("/new/", s.HandleNewQuery) s.mux.HandleFunc("/new/", s.HandleNewQuery)
http.HandleFunc("/new/title/{title}/{chapter}", s.HandleNew) s.mux.HandleFunc("/new/title/{title}/{chapter}", s.HandleNew)
http.HandleFunc("/current/", s.HandleCurrent) s.mux.HandleFunc("/current/", s.HandleCurrent)
http.HandleFunc("/img/{url}/", s.HandleImage) s.mux.HandleFunc("/img/{url}", s.HandleImage)
http.HandleFunc("POST /next", s.HandleNext) s.mux.HandleFunc("POST /next", s.HandleNext)
http.HandleFunc("POST /prev", s.HandlePrev) s.mux.HandleFunc("POST /prev", s.HandlePrev)
http.HandleFunc("POST /exit", s.HandleExit) s.mux.HandleFunc("POST /exit", s.HandleExit)
http.HandleFunc("POST /delete", s.HandleDelete) s.mux.HandleFunc("POST /delete", s.HandleDelete)
http.HandleFunc("/favicon.ico", s.HandleFavicon) s.mux.HandleFunc("/favicon.ico", s.HandleFavicon)
http.HandleFunc("POST /setting/", s.HandleSetting) s.mux.HandleFunc("POST /setting/", s.HandleSetting)
http.HandleFunc("GET /setting/set/{setting}/{value}", s.HandleSettingSet) s.mux.HandleFunc("GET /setting/set/{setting}/{value}", s.HandleSettingSet)
s.mux.HandleFunc("GET /update", s.HandleUpdate)
// Update Latest Chapters every 5 Minutes s.mux.HandleFunc("POST /disable", s.HandleDisable)
// go func(s *Server) { s.mux.HandleFunc("GET /archive", s.HandleArchive)
// time.AfterFunc(time.Second*10, func() {
// var all []*database.Manga
// s.DbMgr.Db.Find(&all)
// for _, m := range all {
// err, updated := s.UpdateLatestAvailableChapter(m)
// if err != nil {
// fmt.Println(err)
// }
// if updated {
// s.DbMgr.Db.Save(m)
// }
// }
// })
//
// for {
// select {
// case <-time.After(time.Minute * 5):
// var all []*database.Manga
// s.DbMgr.Db.Find(&all)
// for _, m := range all {
// err, updated := s.UpdateLatestAvailableChapter(m)
// if err != nil {
// fmt.Println(err)
// }
// if updated {
// s.DbMgr.Db.Save(m)
// }
// }
// }
// }
// }(s)
//
fmt.Println("Server starting...")
err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
return err
} }
func (s *Server) LoadNext(session *UserSession) { func (s *Server) Start() error {
next, err := s.Provider.GetHtml(session.CurrSubUrl) 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 { if err != nil {
fmt.Println(err) log.Error().Err(err).Msg("Could not get Html for current chapter")
session.NextSubUrl = "" s.NextSubUrl = ""
session.NextViewModel = nil s.NextViewModel = nil
return
}
next, err := s.Provider.GetNext(c)
if err != nil {
log.Error().Err(err).Msg("Could not load next chapter")
s.NextSubUrl = ""
s.NextViewModel = nil
return return
} }
html, err := s.Provider.GetHtml(next) html, err := s.Provider.GetHtml(next)
if err != nil { if err != nil {
fmt.Println(err) log.Error().Err(err).Msg("Could not get Html for next chapter")
session.NextSubUrl = "" s.NextSubUrl = ""
session.NextViewModel = nil s.NextViewModel = nil
return return
} }
imagesNext, err := s.AppendImagesToBuf(html) imagesNext, err := s.AppendImagesToBuf(html)
if err != nil { if err != nil {
fmt.Println(err) log.Error().Err(err).Msg("Could not download images")
session.NextSubUrl = "" s.NextSubUrl = ""
session.NextViewModel = nil s.NextViewModel = nil
return return
} }
title, chapter, err := s.Provider.GetTitleAndChapter(next) title, chapter, err := s.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_?"
} }
full := strings.Replace(title, "-", " ", -1) + " - " + strings.Replace(chapter, "_", " ", -1) full := strings.Replace(title, "-", " ", -1) + " - " + strings.Replace(chapter, "_", " ", -1)
session.NextViewModel = &view.ImageViewModel{Images: imagesNext, Title: full} s.NextViewModel = &view.ImageViewModel{Images: imagesNext, Title: full}
session.NextSubUrl = next s.NextSubUrl = next
fmt.Println("Loaded next") log.Debug().Msg("Successfully loaded next chapter")
} }
func (s *Server) LoadPrev(session *UserSession) { func (s *Server) LoadPrev() {
c, err := s.Provider.GetHtml(session.CurrSubUrl) c, err := s.Provider.GetHtml(s.CurrSubUrl)
if err != nil { if err != nil {
fmt.Println(err) log.Error().Err(err).Msg("Could not get Html for current chapter")
session.PrevSubUrl = "" s.PrevSubUrl = ""
session.PrevViewModel = nil s.PrevViewModel = nil
return return
} }
prev, err := s.Provider.GetPrev(c) prev, err := s.Provider.GetPrev(c)
if err != nil { if err != nil {
fmt.Println(err) log.Error().Err(err).Msg("Could not load prev chapter")
session.PrevSubUrl = "" s.PrevSubUrl = ""
session.PrevViewModel = nil s.PrevViewModel = nil
return return
} }
html, err := s.Provider.GetHtml(prev) html, err := s.Provider.GetHtml(prev)
if err != nil { if err != nil {
fmt.Println(err) log.Error().Err(err).Msg("Could not get Html for prev chapter")
session.PrevSubUrl = "" s.PrevSubUrl = ""
session.PrevViewModel = nil s.PrevViewModel = nil
return return
} }
imagesNext, err := s.AppendImagesToBuf(html) imagesNext, err := s.AppendImagesToBuf(html)
if err != nil { if err != nil {
fmt.Println(err) log.Error().Err(err).Msg("Could not download images")
session.PrevSubUrl = "" s.PrevSubUrl = ""
session.PrevViewModel = nil s.PrevViewModel = nil
return return
} }
title, chapter, err := s.Provider.GetTitleAndChapter(prev) title, chapter, err := s.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_?"
} }
full := strings.Replace(title, "-", " ", -1) + " - " + strings.Replace(chapter, "_", " ", -1) full := strings.Replace(title, "-", " ", -1) + " - " + strings.Replace(chapter, "_", " ", -1)
session.PrevViewModel = &view.ImageViewModel{Images: imagesNext, Title: full} s.PrevViewModel = &view.ImageViewModel{Images: imagesNext, Title: full}
s.PrevSubUrl = prev
log.Debug().Msg("Successfully loaded prev chapter")
session.PrevSubUrl = prev
fmt.Println("Loaded prev")
} }
func (s *Server) LoadCurr(session *UserSession) { func (s *Server) LoadCurr() {
html, err := s.Provider.GetHtml(session.CurrSubUrl) html, err := s.Provider.GetHtml(s.CurrSubUrl)
if err != nil { 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) imagesCurr, err := s.AppendImagesToBuf(html)
title, chapter, err := s.Provider.GetTitleAndChapter(session.CurrSubUrl) title, chapter, err := s.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_?"
} }
full := strings.Replace(title, "-", " ", -1) + " - " + strings.Replace(chapter, "_", " ", -1) full := strings.Replace(title, "-", " ", -1) + " - " + strings.Replace(chapter, "_", " ", -1)
session.CurrViewModel = &view.ImageViewModel{Images: imagesCurr, Title: full} s.CurrViewModel = &view.ImageViewModel{Images: imagesCurr, Title: full}
fmt.Println("Loaded current") log.Debug().Msg("Successfully loaded curr chapter")
} }
func (s *Server) UpdateLatestAvailableChapter(manga *database.Manga) (error, bool) { func (s *Server) UpdateLatestAvailableChapter(manga *database.Manga) (error, bool) {
fmt.Printf("Updating Manga: %s\n", manga.Definition.Title) log.Info().Str("Manga", manga.Title).Msg("Updating Manga")
l, err := s.Provider.GetChapterList("/title/" + strconv.Itoa(manga.Definition.Id)) l, err := s.Provider.GetChapterList("/title/" + strconv.Itoa(manga.Id))
if err != nil { if err != nil {
return err, false return err, false
} }
@@ -226,16 +292,16 @@ func (s *Server) UpdateLatestAvailableChapter(manga *database.Manga) (error, boo
chapterNumberStr := strings.Replace(c, "ch_", "", 1) chapterNumberStr := strings.Replace(c, "ch_", "", 1)
if manga.Definition.LastChapterNum == chapterNumberStr { if manga.LastChapterNum == chapterNumberStr {
return nil, false return nil, false
} else { } else {
manga.Definition.LastChapterNum = chapterNumberStr manga.LastChapterNum = chapterNumberStr
return nil, true return nil, true
} }
} }
func (s *Server) LoadThumbnail(manga *database.Manga) (path string, updated bool, err error) { func (s *Server) LoadThumbnail(manga *database.Manga) (path string, updated bool, err error) {
strId := strconv.Itoa(manga.Definition.Id) strId := strconv.Itoa(manga.Id)
s.Mutex.Lock() s.Mutex.Lock()
defer s.Mutex.Unlock() defer s.Mutex.Unlock()
@@ -243,8 +309,8 @@ func (s *Server) LoadThumbnail(manga *database.Manga) (path string, updated bool
return strId, false, nil return strId, false, nil
} }
if manga.Definition.Thumbnail != nil { if manga.Thumbnail != nil {
s.ImageBuffers[strId] = manga.Definition.Thumbnail s.ImageBuffers[strId] = manga.Thumbnail
return strId, false, nil return strId, false, nil
} }
@@ -256,7 +322,7 @@ func (s *Server) LoadThumbnail(manga *database.Manga) (path string, updated bool
if err != nil { if err != nil {
return "", false, err return "", false, err
} }
manga.Definition.Thumbnail = ram manga.Thumbnail = ram
s.ImageBuffers[strId] = ram s.ImageBuffers[strId] = ram
return strId, true, nil return strId, true, nil
} }
@@ -299,7 +365,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 {
fmt.Println(err) log.Error().Err(err).Msg("Could not close http body")
} }
}(resp.Body) }(resp.Body)

View File

@@ -1,31 +0,0 @@
package utils
import "sync"
type ConcurrentMap[K comparable, Value any] struct {
dirty map[K]Value
count int
mutex *sync.RWMutex
}
func NewConcurrentMap[K comparable, V any]() ConcurrentMap[K, V] {
return ConcurrentMap[K, V]{
dirty: make(map[K]V),
count: 0,
mutex: &sync.RWMutex{},
}
}
func (c *ConcurrentMap[K, Value]) Get(key K) (Value, bool) {
c.mutex.RLock()
defer c.mutex.RUnlock()
val, ok := c.dirty[key]
return val, ok
}
func (c *ConcurrentMap[K, Value]) Set(key K, val Value) {
c.mutex.Lock()
defer c.mutex.Unlock()
c.dirty[key] = val
}

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

@@ -14,12 +14,17 @@ 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,6 +13,8 @@ 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

@@ -20,9 +20,11 @@ type MangaViewModel struct {
LastTime string LastTime string
Url string Url string
ThumbnailUrl string ThumbnailUrl string
Enabled bool
} }
type MenuViewModel struct { type MenuViewModel struct {
Archive bool
Settings map[string]database.Setting Settings map[string]database.Setting
Mangas []MangaViewModel Mangas []MangaViewModel
} }

View File

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

148
main.go
View File

@@ -1,49 +1,154 @@
package main package main
import ( import (
"flag"
"fmt" "fmt"
"github.com/pablu23/mangaGetter/internal/database" "io"
"github.com/pablu23/mangaGetter/internal/provider" "net/http"
"github.com/pablu23/mangaGetter/internal/server"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
"runtime" "runtime"
"time" "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() { func main() {
filePath := getDbPath() flag.Parse()
db := database.NewDatabase(filePath, true) setupLogging()
filePath := setupDb()
db := database.NewDatabase(filePath, true, *debugFlag)
err := db.Open() err := db.Open()
if err != nil { if err != nil {
fmt.Println(err) log.Fatal().Err(err).Str("Path", filePath).Msg("Could not open Database")
return
} }
s := server.New(&provider.Bato{}, &db) 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) c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt) signal.Notify(c, os.Interrupt)
go func() { go func() {
for range c { for range c {
Close(&db) Close(db)
} }
}() }()
}
go func() { func setupDb() string {
time.Sleep(300 * time.Millisecond) if *databaseFlag != "" {
err := open(fmt.Sprintf("http://localhost:%d", port)) return *databaseFlag
if err != nil { } else {
fmt.Println(err) 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,
err = s.Start(port) MaxAge: 14,
if err != nil { MaxBackups: 10,
panic(err) }))
} }
} }
@@ -65,10 +170,11 @@ func open(url string) error {
} }
func Close(db *database.Manager) { func Close(db *database.Manager) {
fmt.Println("Attempting to save and close DB") log.Debug().Msg("Closing Database")
err := db.Close() err := db.Close()
if err != nil { if err != nil {
fmt.Println(err) log.Error().Err(err).Msg("Could not close Database")
return
} }
os.Exit(0) os.Exit(0)
} }

Binary file not shown.

View File

@@ -7,7 +7,31 @@ import (
"path/filepath" "path/filepath"
) )
const port = 8000 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 { func getDbPath() string {
dir, err := os.UserCacheDir() dir, err := os.UserCacheDir()

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"}