Compare commits
23 Commits
Multiple-U
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
282e2e6173 | ||
|
|
2f224e75c5 | ||
|
|
45a0d9c150 | ||
|
|
b3fcdfd174 | ||
|
|
99c2e4495d | ||
|
|
48b2e6787b | ||
|
|
05ca6c9b1b | ||
|
|
b0c204ec68 | ||
|
|
8f65d81015 | ||
|
|
dbe349d0e6 | ||
|
|
b46d00a873 | ||
|
|
e5e7c4eb54 | ||
|
|
766da5aeb2 | ||
|
|
2aab7906e1 | ||
|
|
124dab097a | ||
|
|
16be88a849 | ||
|
|
8de5bb0fdc | ||
|
|
e9c7c6f915 | ||
|
|
11857a7156 | ||
|
|
a2232026a0 | ||
|
|
f905d482a5 | ||
|
|
392114b240 | ||
|
|
63ffb8df6e |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -6,3 +6,7 @@ h.html
|
||||
*.bak
|
||||
/bin
|
||||
*.exe
|
||||
*secret*
|
||||
mangaGetter
|
||||
*.crt
|
||||
*.key
|
||||
|
||||
3
Makefile
3
Makefile
@@ -1,6 +1,5 @@
|
||||
run: develop
|
||||
bin/develop
|
||||
|
||||
bin/develop --secret test --server --port 8181 --database db.sqlite --debug --pretty
|
||||
develop:
|
||||
go build -tags Develop -o bin/develop
|
||||
release:
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
|
||||
package main
|
||||
|
||||
const port = 8080
|
||||
func getSecretPath() (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func getSecret() (string, error) {
|
||||
return "test", nil
|
||||
}
|
||||
|
||||
func getDbPath() string {
|
||||
return "db.sqlite"
|
||||
|
||||
9
go.mod
9
go.mod
@@ -7,9 +7,18 @@ require (
|
||||
golang.org/x/text v0.14.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/rs/zerolog v1.33.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gorm.io/driver/sqlite v1.5.5
|
||||
gorm.io/gorm v1.25.10
|
||||
)
|
||||
|
||||
23
go.sum
23
go.sum
@@ -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/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E=
|
||||
gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE=
|
||||
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
package database
|
||||
|
||||
type Chapter struct {
|
||||
Id int `gorm:"primary_key;autoIncrement;"`
|
||||
ChapterId int
|
||||
Id int `gorm:"primary_key;AUTO_INCREMENT"`
|
||||
Url string
|
||||
Name string
|
||||
Number string
|
||||
TimeStampUnix int64
|
||||
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{
|
||||
ChapterId: id,
|
||||
Id: id,
|
||||
Url: url,
|
||||
Name: name,
|
||||
Number: number,
|
||||
TimeStampUnix: timeStampUnix,
|
||||
MangaId: mangaId,
|
||||
UserId: userId,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,30 +2,46 @@ package database
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
ConnectionString string
|
||||
Db *gorm.DB
|
||||
CreateIfNotExists bool
|
||||
ActivateGormLogger bool
|
||||
}
|
||||
|
||||
func NewDatabase(connectionString string, createIfNotExists bool) Manager {
|
||||
func NewDatabase(connectionString string, createIfNotExists bool, activateGormLogger bool) Manager {
|
||||
return Manager{
|
||||
ConnectionString: connectionString,
|
||||
Db: nil,
|
||||
CreateIfNotExists: createIfNotExists,
|
||||
ActivateGormLogger: activateGormLogger,
|
||||
}
|
||||
}
|
||||
|
||||
func (dbMgr *Manager) Open() error {
|
||||
db, err := gorm.Open(sqlite.Open(dbMgr.ConnectionString), &gorm.Config{})
|
||||
var db *gorm.DB
|
||||
var err error
|
||||
if dbMgr.ActivateGormLogger {
|
||||
db, err = gorm.Open(sqlite.Open(dbMgr.ConnectionString), &gorm.Config{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
db, err = gorm.Open(sqlite.Open(dbMgr.ConnectionString), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
dbMgr.Db = db
|
||||
if dbMgr.CreateIfNotExists {
|
||||
err = dbMgr.createDatabaseIfNotExists()
|
||||
@@ -54,6 +70,6 @@ func (dbMgr *Manager) Delete(mangaId int) {
|
||||
}
|
||||
|
||||
func (dbMgr *Manager) createDatabaseIfNotExists() error {
|
||||
err := dbMgr.Db.AutoMigrate(&MangaDefinition{}, &User{}, &Manga{}, &Chapter{}, &Setting{})
|
||||
err := dbMgr.Db.AutoMigrate(&Manga{}, &Chapter{}, &Setting{})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,58 +1,42 @@
|
||||
package database
|
||||
|
||||
type MangaDefinition struct {
|
||||
Id int `gorm:"primary_key;AUTO_INCREMENT"`
|
||||
Title string
|
||||
Thumbnail []byte
|
||||
LastChapterNum string
|
||||
// Chapters []Chapter
|
||||
//`gorm:"foreignkey:MangaID"`
|
||||
}
|
||||
|
||||
type Manga struct {
|
||||
Id int `gorm:"primary_key;AUTO_INCREMENT"`
|
||||
MangaDefinitionId int
|
||||
Definition MangaDefinition `gorm:"foreignKey:MangaDefinitionId"`
|
||||
UserId int
|
||||
User User
|
||||
Title string
|
||||
TimeStampUnix int64
|
||||
Chapters []Chapter `gorm:"foreignKey:MangaId"`
|
||||
Thumbnail []byte
|
||||
LastChapterNum string
|
||||
Chapters []Chapter
|
||||
Enabled bool
|
||||
//`gorm:"foreignkey:MangaID"`
|
||||
}
|
||||
|
||||
func NewMangaDefinition(id int, title string) MangaDefinition {
|
||||
return MangaDefinition{
|
||||
func NewManga(id int, title string, timeStampUnix int64) Manga {
|
||||
return Manga{
|
||||
Id: id,
|
||||
Title: title,
|
||||
LastChapterNum: "",
|
||||
}
|
||||
}
|
||||
|
||||
func NewManga(def MangaDefinition, user User, timeStampUnix int64) Manga {
|
||||
return Manga{
|
||||
MangaDefinitionId: def.Id,
|
||||
Definition: def,
|
||||
UserId: user.Id,
|
||||
User: user,
|
||||
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.Manga.Id && highest < chapter.TimeStampUnix {
|
||||
// highest = chapter.TimeStampUnix
|
||||
// index = i
|
||||
// }
|
||||
// }
|
||||
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 {
|
||||
if highest == 0 {
|
||||
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)
|
||||
//if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
|
||||
@@ -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"`
|
||||
// }
|
||||
@@ -7,10 +7,19 @@ import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type Bato struct{}
|
||||
|
||||
func (b *Bato) CleanUrlToSub(url string) string {
|
||||
trimmed := strings.TrimPrefix(url, "https://bato.to/title")
|
||||
trimmed = strings.Trim(trimmed, "/")
|
||||
return trimmed
|
||||
}
|
||||
|
||||
func (b *Bato) GetImageList(html string) ([]string, error) {
|
||||
reg, err := regexp.Compile(`<astro-island.*props=".*;imageFiles":\[1,"\[(.*)]"]`)
|
||||
if err != nil {
|
||||
@@ -49,7 +58,7 @@ func (b *Bato) GetHtml(titleSubUrl string) (string, error) {
|
||||
defer func(Body io.ReadCloser) {
|
||||
err := Body.Close()
|
||||
if err != nil {
|
||||
fmt.Printf("Could not close body because: %v\n", err)
|
||||
log.Error().Err(err).Msg("Could not close http body")
|
||||
}
|
||||
}(resp.Body)
|
||||
|
||||
@@ -64,14 +73,23 @@ func (b *Bato) GetHtml(titleSubUrl string) (string, error) {
|
||||
|
||||
func (b *Bato) GetNext(html string) (subUrl string, err error) {
|
||||
reg, err := regexp.Compile(`<a data-hk="0-6-0" .*? href="(.*?)["']`)
|
||||
match := reg.FindStringSubmatch(html)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
match := reg.FindStringSubmatch(html)
|
||||
if len(match) <= 1 {
|
||||
return "", err
|
||||
}
|
||||
return match[1], err
|
||||
}
|
||||
|
||||
func (b *Bato) GetPrev(html string) (subUrl string, err error) {
|
||||
reg, err := regexp.Compile(`<a data-hk="0-5-0" .*? href="(.*?)["']`)
|
||||
match := reg.FindStringSubmatch(html)
|
||||
if len(match) <= 1 {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return match[1], err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package provider
|
||||
|
||||
type Provider interface {
|
||||
CleanUrlToSub(url string) string
|
||||
GetImageList(html string) (imageUrls []string, err error)
|
||||
GetHtml(url string) (html string, err error)
|
||||
GetNext(html string) (url string, err error)
|
||||
|
||||
@@ -14,70 +14,52 @@ import (
|
||||
|
||||
"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) getSessionFromCookie(w http.ResponseWriter, r *http.Request) (*UserSession, error) {
|
||||
cookie, err := r.Cookie("session")
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, http.ErrNoCookie):
|
||||
// http.Error(w, "cookie not found", http.StatusBadRequest)
|
||||
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
|
||||
default:
|
||||
fmt.Println(err)
|
||||
http.Error(w, "server error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
session, ok := s.Sessions[cookie.Value]
|
||||
if !ok {
|
||||
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
|
||||
return nil, errors.New("Unknown Session")
|
||||
}
|
||||
return session, err
|
||||
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)
|
||||
}
|
||||
|
||||
func (s *Server) HandleRegister(w http.ResponseWriter, r *http.Request) {
|
||||
s.Mutex.Lock()
|
||||
defer s.Mutex.Unlock()
|
||||
|
||||
admin := database.User{
|
||||
Id: 1,
|
||||
DisplayName: "admin",
|
||||
LoginName: "admin",
|
||||
manga.Enabled = !manga.Enabled
|
||||
s.DbMgr.Db.Save(&manga)
|
||||
}
|
||||
s.DbMgr.Db.Create(&admin)
|
||||
|
||||
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
|
||||
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) {
|
||||
s.Mutex.Lock()
|
||||
defer s.Mutex.Unlock()
|
||||
|
||||
// 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)
|
||||
tmpl := template.Must(view.GetViewTemplate(view.Login))
|
||||
tmpl.Execute(w, nil)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
session, err := s.getSessionFromCookie(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
s.CurrSubUrl = url
|
||||
s.PrevSubUrl = ""
|
||||
s.NextSubUrl = ""
|
||||
s.LoadCurr()
|
||||
|
||||
go s.LoadNext()
|
||||
go s.LoadPrev()
|
||||
|
||||
http.Redirect(w, r, "/current/", http.StatusFound)
|
||||
}
|
||||
|
||||
session.CurrSubUrl = url
|
||||
session.PrevSubUrl = ""
|
||||
session.NextSubUrl = ""
|
||||
s.LoadCurr(session)
|
||||
|
||||
go s.LoadNext(session)
|
||||
go s.LoadPrev(session)
|
||||
|
||||
http.Redirect(w, r, "/current/", http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
func (s *Server) HandleMenu(w http.ResponseWriter, r *http.Request) {
|
||||
tmpl := template.Must(view.GetViewTemplate(view.Menu))
|
||||
|
||||
session, err := s.getSessionFromCookie(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
func (s *Server) HandleArchive(w http.ResponseWriter, r *http.Request) {
|
||||
var all []*database.Manga
|
||||
_ = s.DbMgr.Db.Preload("Chapters").Where("user_id = ?", session.User.Id).Find(&all)
|
||||
l := len(all)
|
||||
mangaViewModels := make([]view.MangaViewModel, l)
|
||||
counter := 0
|
||||
|
||||
n := time.Now().UnixNano()
|
||||
_ = s.DbMgr.Db.Preload("Chapters").Where("enabled = 0").Find(&all)
|
||||
|
||||
var tmp []database.Setting
|
||||
s.DbMgr.Db.Find(&tmp)
|
||||
@@ -124,14 +90,33 @@ func (s *Server) HandleMenu(w http.ResponseWriter, r *http.Request) {
|
||||
settings[m.Name] = m
|
||||
}
|
||||
|
||||
var thumbNs int64 = 0
|
||||
var titNs int64 = 0
|
||||
s.ViewMenu(w, all, settings, true)
|
||||
}
|
||||
|
||||
func (s *Server) HandleMenu(w http.ResponseWriter, _ *http.Request) {
|
||||
var all []*database.Manga
|
||||
_ = s.DbMgr.Db.Preload("Chapters").Where("enabled = 1").Find(&all)
|
||||
|
||||
var tmp []database.Setting
|
||||
s.DbMgr.Db.Find(&tmp)
|
||||
settings := make(map[string]database.Setting)
|
||||
for _, m := range tmp {
|
||||
settings[m.Name] = m
|
||||
}
|
||||
|
||||
s.ViewMenu(w, all, settings, false)
|
||||
}
|
||||
|
||||
func (s *Server) ViewMenu(w http.ResponseWriter, mangas []*database.Manga, settings map[string]database.Setting, archive bool) {
|
||||
tmpl := template.Must(view.GetViewTemplate(view.Menu))
|
||||
|
||||
l := len(mangas)
|
||||
mangaViewModels := make([]view.MangaViewModel, l)
|
||||
counter := 0
|
||||
|
||||
//TODO: Change all this to be more performant
|
||||
for _, manga := range all {
|
||||
title := cases.Title(language.English, cases.Compact).String(strings.Replace(manga.Definition.Title, "-", " ", -1))
|
||||
|
||||
t1 := time.Now().UnixNano()
|
||||
for _, manga := range mangas {
|
||||
title := cases.Title(language.English, cases.Compact).String(strings.Replace(manga.Title, "-", " ", -1))
|
||||
|
||||
thumbnail, updated, err := s.LoadThumbnail(manga)
|
||||
//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 {
|
||||
s.DbMgr.Db.Save(manga)
|
||||
}
|
||||
|
||||
t2 := time.Now().UnixNano()
|
||||
|
||||
thumbNs += t2 - t1
|
||||
|
||||
t1 = time.Now().UnixNano()
|
||||
|
||||
// This is very slow
|
||||
// TODO: put this into own Method
|
||||
if manga.Definition.LastChapterNum == "" {
|
||||
if manga.LastChapterNum == "" {
|
||||
err, updated := s.UpdateLatestAvailableChapter(manga)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
log.Error().Err(err).Msg("Could not update latest available chapters")
|
||||
}
|
||||
if updated {
|
||||
s.DbMgr.Db.Save(manga.Definition)
|
||||
s.DbMgr.Db.Save(manga)
|
||||
}
|
||||
}
|
||||
|
||||
t2 = time.Now().UnixNano()
|
||||
|
||||
titNs += t2 - t1
|
||||
latestChapter, ok := manga.GetLatestChapter()
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
mangaViewModels[counter] = view.MangaViewModel{
|
||||
ID: manga.Definition.Id,
|
||||
ID: manga.Id,
|
||||
Title: title,
|
||||
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
|
||||
LastTime: time.Unix(manga.TimeStampUnix, 0).Format("15:04 (02-01-06)"),
|
||||
Url: latestChapter.Url,
|
||||
ThumbnailUrl: thumbnail,
|
||||
Enabled: manga.Enabled,
|
||||
}
|
||||
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"]
|
||||
if !ok || order.Value == "title" {
|
||||
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{
|
||||
Settings: settings,
|
||||
Mangas: mangaViewModels,
|
||||
Archive: archive,
|
||||
}
|
||||
|
||||
err = tmpl.Execute(w, menuViewModel)
|
||||
err := tmpl.Execute(w, menuViewModel)
|
||||
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")
|
||||
|
||||
if mangaStr == "" {
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
mangaId, err := strconv.Atoi(mangaStr)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
log.Error().Err(err).Str("Id", mangaStr).Msg("Could not convert id to int")
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
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) {
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
|
||||
// session, err := s.getSessionFromCookie(w, r)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
|
||||
go func() {
|
||||
// session.Mutex.Lock()
|
||||
// if session.PrevViewModel != nil {
|
||||
// for _, img := range session.PrevViewModel.Images {
|
||||
// delete(s.ImageBuffers, img.Path)
|
||||
// }
|
||||
// }
|
||||
// if session.CurrViewModel != nil {
|
||||
//
|
||||
// for _, img := range session.CurrViewModel.Images {
|
||||
// delete(s.ImageBuffers, img.Path)
|
||||
// }
|
||||
// }
|
||||
// if session.NextViewModel != nil {
|
||||
//
|
||||
// for _, img := range session.NextViewModel.Images {
|
||||
// delete(s.ImageBuffers, img.Path)
|
||||
// }
|
||||
// }
|
||||
// session.Mutex.Unlock()
|
||||
fmt.Println("Cleaned last Manga")
|
||||
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))
|
||||
session, err := s.getSessionFromCookie(w, r)
|
||||
mangaId, chapterId, err := s.Provider.GetTitleIdAndChapterId(s.CurrSubUrl)
|
||||
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
|
||||
}
|
||||
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 {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
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)
|
||||
log.Warn().Err(err).Str("subUrl", s.CurrSubUrl).Msg("Could not get Title and Chapter")
|
||||
}
|
||||
|
||||
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) {
|
||||
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
|
||||
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) {
|
||||
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 {
|
||||
chapter.TimeStampUnix = time.Now().Unix()
|
||||
}
|
||||
|
||||
s.DbMgr.Db.Save(&mangaDef)
|
||||
s.DbMgr.Db.Save(&manga)
|
||||
s.DbMgr.Db.Save(&chapter)
|
||||
|
||||
err = tmpl.Execute(w, session.CurrViewModel)
|
||||
err = tmpl.Execute(w, s.CurrViewModel)
|
||||
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) {
|
||||
u := r.PathValue("url")
|
||||
s.Mutex.RLock()
|
||||
defer s.Mutex.RUnlock()
|
||||
s.Mutex.Lock()
|
||||
defer s.Mutex.Unlock()
|
||||
buf := s.ImageBuffers[u]
|
||||
if buf == nil {
|
||||
fmt.Printf("url: %s is nil\n", u)
|
||||
w.WriteHeader(400)
|
||||
log.Warn().Str("url", u).Msg("Image not found")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "image/webp")
|
||||
_, err := w.Write(buf)
|
||||
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")
|
||||
_, err := w.Write(ico)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
log.Error().Err(err).Msg("Could not write favicon")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) HandleNext(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Println("Received Next")
|
||||
|
||||
session, err := s.getSessionFromCookie(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if session.PrevViewModel != nil {
|
||||
if s.PrevViewModel != nil {
|
||||
go func(viewModel view.ImageViewModel, s *Server) {
|
||||
s.Mutex.Lock()
|
||||
for _, img := range viewModel.Images {
|
||||
delete(s.ImageBuffers, img.Path)
|
||||
}
|
||||
s.Mutex.Unlock()
|
||||
fmt.Println("Cleaned out of scope Last")
|
||||
}(*session.PrevViewModel, s)
|
||||
log.Debug().Msg("Cleaned imagebuffer")
|
||||
}(*s.PrevViewModel, s)
|
||||
}
|
||||
|
||||
if session.NextViewModel == nil || session.NextSubUrl == "" {
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
if s.NextViewModel == nil || s.NextSubUrl == "" {
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
session.PrevViewModel = session.CurrViewModel
|
||||
session.CurrViewModel = session.NextViewModel
|
||||
session.PrevSubUrl = session.CurrSubUrl
|
||||
session.CurrSubUrl = session.NextSubUrl
|
||||
s.PrevViewModel = s.CurrViewModel
|
||||
s.CurrViewModel = s.NextViewModel
|
||||
s.PrevSubUrl = s.CurrSubUrl
|
||||
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)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if session.NextViewModel != nil {
|
||||
if s.NextViewModel != nil {
|
||||
go func(viewModel view.ImageViewModel, s *Server) {
|
||||
s.Mutex.Lock()
|
||||
for _, img := range viewModel.Images {
|
||||
delete(s.ImageBuffers, img.Path)
|
||||
}
|
||||
s.Mutex.Unlock()
|
||||
fmt.Println("Cleaned out of scope Last")
|
||||
}(*session.NextViewModel, s)
|
||||
log.Debug().Msg("Cleaned imagebuffer")
|
||||
}(*s.NextViewModel, s)
|
||||
}
|
||||
|
||||
if session.PrevViewModel == nil || session.PrevSubUrl == "" {
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
if s.PrevViewModel == nil || s.PrevSubUrl == "" {
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
session.NextViewModel = session.CurrViewModel
|
||||
session.CurrViewModel = session.PrevViewModel
|
||||
session.NextSubUrl = session.CurrSubUrl
|
||||
session.CurrSubUrl = session.PrevSubUrl
|
||||
s.NextViewModel = s.CurrViewModel
|
||||
s.CurrViewModel = s.PrevViewModel
|
||||
s.NextSubUrl = s.CurrSubUrl
|
||||
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) {
|
||||
@@ -434,7 +376,7 @@ func (s *Server) HandleSettingSet(w http.ResponseWriter, r *http.Request) {
|
||||
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) {
|
||||
@@ -447,29 +389,28 @@ func (s *Server) HandleSetting(w http.ResponseWriter, r *http.Request) {
|
||||
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.StatusTemporaryRedirect)
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
}
|
||||
|
||||
func (s *Server) HandleNewQuery(w http.ResponseWriter, r *http.Request) {
|
||||
sub := r.PostFormValue("subUrl")
|
||||
|
||||
sub = s.Provider.CleanUrlToSub(sub)
|
||||
url := fmt.Sprintf("/title/%s", sub)
|
||||
|
||||
session, err := s.getSessionFromCookie(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
session.CurrSubUrl = url
|
||||
session.PrevSubUrl = ""
|
||||
session.NextSubUrl = ""
|
||||
s.LoadCurr(session)
|
||||
s.CurrSubUrl = url
|
||||
s.PrevSubUrl = ""
|
||||
s.NextSubUrl = ""
|
||||
s.LoadCurr()
|
||||
|
||||
go s.LoadNext(session)
|
||||
go s.LoadPrev(session)
|
||||
go s.LoadNext()
|
||||
go s.LoadPrev()
|
||||
|
||||
http.Redirect(w, r, "/current/", http.StatusTemporaryRedirect)
|
||||
http.Redirect(w, r, "/current/", http.StatusFound)
|
||||
}
|
||||
|
||||
20
internal/server/middleware.go
Normal file
20
internal/server/middleware.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
62
internal/server/options.go
Normal file
62
internal/server/options.go
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -2,218 +2,284 @@ package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pablu23/mangaGetter/internal/database"
|
||||
"github.com/pablu23/mangaGetter/internal/provider"
|
||||
"github.com/pablu23/mangaGetter/internal/view"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
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
|
||||
CurrViewModel *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, mux *http.ServeMux, options ...func(*Options)) *Server {
|
||||
opts := NewDefaultOptions()
|
||||
for _, opt := range options {
|
||||
opt(&opts)
|
||||
}
|
||||
|
||||
func New(provider provider.Provider, db *database.Manager) *Server {
|
||||
s := Server{
|
||||
ImageBuffers: make(map[string][]byte),
|
||||
Sessions: make(map[string]*UserSession),
|
||||
Provider: provider,
|
||||
DbMgr: db,
|
||||
Mutex: &sync.RWMutex{},
|
||||
Mutex: &sync.Mutex{},
|
||||
mux: mux,
|
||||
options: opts,
|
||||
}
|
||||
|
||||
return &s
|
||||
}
|
||||
|
||||
func (s *Server) Start(port int) error {
|
||||
http.HandleFunc("/register", s.HandleRegister)
|
||||
http.HandleFunc("/login", s.HandleLogin)
|
||||
http.HandleFunc("/", s.HandleMenu)
|
||||
http.HandleFunc("/new/", s.HandleNewQuery)
|
||||
http.HandleFunc("/new/title/{title}/{chapter}", s.HandleNew)
|
||||
http.HandleFunc("/current/", s.HandleCurrent)
|
||||
http.HandleFunc("/img/{url}/", s.HandleImage)
|
||||
http.HandleFunc("POST /next", s.HandleNext)
|
||||
http.HandleFunc("POST /prev", s.HandlePrev)
|
||||
http.HandleFunc("POST /exit", s.HandleExit)
|
||||
http.HandleFunc("POST /delete", s.HandleDelete)
|
||||
http.HandleFunc("/favicon.ico", s.HandleFavicon)
|
||||
http.HandleFunc("POST /setting/", s.HandleSetting)
|
||||
http.HandleFunc("GET /setting/set/{setting}/{value}", s.HandleSettingSet)
|
||||
|
||||
// Update Latest Chapters every 5 Minutes
|
||||
// go func(s *Server) {
|
||||
// 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) 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) LoadNext(session *UserSession) {
|
||||
next, err := s.Provider.GetHtml(session.CurrSubUrl)
|
||||
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 {
|
||||
fmt.Println(err)
|
||||
session.NextSubUrl = ""
|
||||
session.NextViewModel = 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 {
|
||||
log.Error().Err(err).Msg("Could not get Html for current chapter")
|
||||
s.NextSubUrl = ""
|
||||
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
|
||||
}
|
||||
|
||||
html, err := s.Provider.GetHtml(next)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
session.NextSubUrl = ""
|
||||
session.NextViewModel = nil
|
||||
log.Error().Err(err).Msg("Could not get Html for next chapter")
|
||||
s.NextSubUrl = ""
|
||||
s.NextViewModel = nil
|
||||
return
|
||||
}
|
||||
|
||||
imagesNext, err := s.AppendImagesToBuf(html)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
session.NextSubUrl = ""
|
||||
session.NextViewModel = nil
|
||||
log.Error().Err(err).Msg("Could not download images")
|
||||
s.NextSubUrl = ""
|
||||
s.NextViewModel = nil
|
||||
return
|
||||
}
|
||||
|
||||
title, chapter, err := s.Provider.GetTitleAndChapter(next)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("Url", next).Msg("Could not extract title and chapter")
|
||||
title = "Unknown"
|
||||
chapter = "ch_?"
|
||||
}
|
||||
|
||||
full := strings.Replace(title, "-", " ", -1) + " - " + strings.Replace(chapter, "_", " ", -1)
|
||||
|
||||
session.NextViewModel = &view.ImageViewModel{Images: imagesNext, Title: full}
|
||||
session.NextSubUrl = next
|
||||
fmt.Println("Loaded next")
|
||||
s.NextViewModel = &view.ImageViewModel{Images: imagesNext, Title: full}
|
||||
s.NextSubUrl = next
|
||||
log.Debug().Msg("Successfully loaded next chapter")
|
||||
}
|
||||
|
||||
func (s *Server) LoadPrev(session *UserSession) {
|
||||
c, err := s.Provider.GetHtml(session.CurrSubUrl)
|
||||
func (s *Server) LoadPrev() {
|
||||
c, err := s.Provider.GetHtml(s.CurrSubUrl)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
session.PrevSubUrl = ""
|
||||
session.PrevViewModel = nil
|
||||
log.Error().Err(err).Msg("Could not get Html for current chapter")
|
||||
s.PrevSubUrl = ""
|
||||
s.PrevViewModel = nil
|
||||
return
|
||||
}
|
||||
prev, err := s.Provider.GetPrev(c)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
session.PrevSubUrl = ""
|
||||
session.PrevViewModel = nil
|
||||
log.Error().Err(err).Msg("Could not load prev chapter")
|
||||
s.PrevSubUrl = ""
|
||||
s.PrevViewModel = nil
|
||||
return
|
||||
}
|
||||
html, err := s.Provider.GetHtml(prev)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
session.PrevSubUrl = ""
|
||||
session.PrevViewModel = nil
|
||||
log.Error().Err(err).Msg("Could not get Html for prev chapter")
|
||||
s.PrevSubUrl = ""
|
||||
s.PrevViewModel = nil
|
||||
return
|
||||
}
|
||||
|
||||
imagesNext, err := s.AppendImagesToBuf(html)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
session.PrevSubUrl = ""
|
||||
session.PrevViewModel = nil
|
||||
log.Error().Err(err).Msg("Could not download images")
|
||||
s.PrevSubUrl = ""
|
||||
s.PrevViewModel = nil
|
||||
return
|
||||
}
|
||||
|
||||
title, chapter, err := s.Provider.GetTitleAndChapter(prev)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("Url", prev).Msg("Could not extract title and chapter")
|
||||
title = "Unknown"
|
||||
chapter = "ch_?"
|
||||
}
|
||||
|
||||
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) {
|
||||
html, err := s.Provider.GetHtml(session.CurrSubUrl)
|
||||
func (s *Server) LoadCurr() {
|
||||
html, err := s.Provider.GetHtml(s.CurrSubUrl)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Error().Err(err).Msg("Could not get Html for current chapter")
|
||||
s.NextSubUrl = ""
|
||||
s.PrevSubUrl = ""
|
||||
s.CurrSubUrl = ""
|
||||
s.NextViewModel = nil
|
||||
s.CurrViewModel = nil
|
||||
s.PrevViewModel = nil
|
||||
return
|
||||
}
|
||||
|
||||
imagesCurr, err := s.AppendImagesToBuf(html)
|
||||
|
||||
title, chapter, err := s.Provider.GetTitleAndChapter(session.CurrSubUrl)
|
||||
title, chapter, err := s.Provider.GetTitleAndChapter(s.CurrSubUrl)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("Url", s.CurrSubUrl).Msg("Could not extract title and chapter")
|
||||
title = "Unknown"
|
||||
chapter = "ch_?"
|
||||
}
|
||||
|
||||
full := strings.Replace(title, "-", " ", -1) + " - " + strings.Replace(chapter, "_", " ", -1)
|
||||
|
||||
session.CurrViewModel = &view.ImageViewModel{Images: imagesCurr, Title: full}
|
||||
fmt.Println("Loaded current")
|
||||
s.CurrViewModel = &view.ImageViewModel{Images: imagesCurr, Title: full}
|
||||
log.Debug().Msg("Successfully loaded curr chapter")
|
||||
}
|
||||
|
||||
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 {
|
||||
return err, false
|
||||
}
|
||||
@@ -226,16 +292,16 @@ func (s *Server) UpdateLatestAvailableChapter(manga *database.Manga) (error, boo
|
||||
|
||||
chapterNumberStr := strings.Replace(c, "ch_", "", 1)
|
||||
|
||||
if manga.Definition.LastChapterNum == chapterNumberStr {
|
||||
if manga.LastChapterNum == chapterNumberStr {
|
||||
return nil, false
|
||||
} else {
|
||||
manga.Definition.LastChapterNum = chapterNumberStr
|
||||
manga.LastChapterNum = chapterNumberStr
|
||||
return nil, true
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
defer s.Mutex.Unlock()
|
||||
@@ -243,8 +309,8 @@ func (s *Server) LoadThumbnail(manga *database.Manga) (path string, updated bool
|
||||
return strId, false, nil
|
||||
}
|
||||
|
||||
if manga.Definition.Thumbnail != nil {
|
||||
s.ImageBuffers[strId] = manga.Definition.Thumbnail
|
||||
if manga.Thumbnail != nil {
|
||||
s.ImageBuffers[strId] = manga.Thumbnail
|
||||
return strId, false, nil
|
||||
}
|
||||
|
||||
@@ -256,7 +322,7 @@ func (s *Server) LoadThumbnail(manga *database.Manga) (path string, updated bool
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
manga.Definition.Thumbnail = ram
|
||||
manga.Thumbnail = ram
|
||||
s.ImageBuffers[strId] = ram
|
||||
return strId, true, nil
|
||||
}
|
||||
@@ -299,7 +365,7 @@ func addFileToRam(url string) ([]byte, error) {
|
||||
defer func(Body io.ReadCloser) {
|
||||
err := Body.Close()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
log.Error().Err(err).Msg("Could not close http body")
|
||||
}
|
||||
}(resp.Body)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
80
internal/view/Views/login.gohtml
Normal file
80
internal/view/Views/login.gohtml
Normal 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>
|
||||
@@ -1,9 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<!--suppress CssUnusedSymbol -->
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Main Menu</title>
|
||||
<title>{{if .Archive}}Archive{{else}}Main Menu{{end}}</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
@@ -53,6 +54,7 @@
|
||||
.button-36 {
|
||||
padding: 0 2.6rem;
|
||||
}
|
||||
|
||||
.button-delete {
|
||||
padding: 0 2.6rem;
|
||||
}
|
||||
@@ -89,10 +91,14 @@
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
border: 1px solid #ddd; /* Gray border */
|
||||
border-radius: 4px; /* Rounded border */
|
||||
padding: 5px; /* Some padding */
|
||||
width: 150px; /* Set a small width */
|
||||
border: 1px solid #ddd;
|
||||
/* Gray border */
|
||||
border-radius: 4px;
|
||||
/* Rounded border */
|
||||
padding: 5px;
|
||||
/* Some padding */
|
||||
width: 150px;
|
||||
/* Set a small width */
|
||||
}
|
||||
|
||||
.thumbnail:hover {
|
||||
@@ -119,10 +125,10 @@
|
||||
margin-bottom: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body class="{{(index .Settings "theme").Value}}">
|
||||
|
||||
<body class='{{(index .Settings "theme").Value}}'>
|
||||
<form method="post" action="/new/">
|
||||
<label>
|
||||
New Sub Url
|
||||
@@ -131,6 +137,24 @@
|
||||
<input type="submit" value="Open" class="button-36">
|
||||
</form>
|
||||
|
||||
<a href='{{if .Archive}}/{{else}}/archive{{end}}'>
|
||||
<button class="button-36">
|
||||
{{if .Archive}}
|
||||
To Main Menu
|
||||
{{else}}
|
||||
To Archive
|
||||
{{end}}
|
||||
</button>
|
||||
</a>
|
||||
|
||||
{{if not .Archive}}
|
||||
<a href="/update">
|
||||
<button class="button-36">
|
||||
Update Chapters
|
||||
</button>
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
<form method="post" action="/setting/">
|
||||
<label for="theme">Theme</label>
|
||||
<select onchange="this.form.submit()" id="theme" name="theme">
|
||||
@@ -147,6 +171,7 @@
|
||||
<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}}
|
||||
@@ -166,6 +191,12 @@
|
||||
</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}}">
|
||||
@@ -176,4 +207,5 @@
|
||||
{{end}}
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -14,12 +14,17 @@ var menu string
|
||||
//go:embed Views/viewer.gohtml
|
||||
var viewer string
|
||||
|
||||
//go:embed Views/login.gohtml
|
||||
var login string
|
||||
|
||||
func GetViewTemplate(view View) (*template.Template, error) {
|
||||
switch view {
|
||||
case Menu:
|
||||
return template.New("menu").Parse(menu)
|
||||
case Viewer:
|
||||
return template.New("viewer").Parse(viewer)
|
||||
case Login:
|
||||
return template.New("login").Parse(login)
|
||||
}
|
||||
return nil, errors.New("invalid view")
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ func GetViewTemplate(view View) (*template.Template, error) {
|
||||
path = "internal/view/Views/menu.gohtml"
|
||||
case Viewer:
|
||||
path = "internal/view/Views/viewer.gohtml"
|
||||
case Login:
|
||||
path = "internal/view/Views/login.gohtml"
|
||||
}
|
||||
return template.ParseFiles(path)
|
||||
}
|
||||
|
||||
@@ -20,9 +20,11 @@ type MangaViewModel struct {
|
||||
LastTime string
|
||||
Url string
|
||||
ThumbnailUrl string
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
type MenuViewModel struct {
|
||||
Archive bool
|
||||
Settings map[string]database.Setting
|
||||
Mangas []MangaViewModel
|
||||
}
|
||||
|
||||
@@ -5,4 +5,5 @@ type View int
|
||||
const (
|
||||
Menu View = iota
|
||||
Viewer View = iota
|
||||
Login View = iota
|
||||
)
|
||||
|
||||
148
main.go
148
main.go
@@ -1,49 +1,154 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/pablu23/mangaGetter/internal/database"
|
||||
"github.com/pablu23/mangaGetter/internal/provider"
|
||||
"github.com/pablu23/mangaGetter/internal/server"
|
||||
"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() {
|
||||
filePath := getDbPath()
|
||||
flag.Parse()
|
||||
|
||||
db := database.NewDatabase(filePath, true)
|
||||
setupLogging()
|
||||
|
||||
filePath := setupDb()
|
||||
db := database.NewDatabase(filePath, true, *debugFlag)
|
||||
err := db.Open()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
log.Fatal().Err(err).Str("Path", filePath).Msg("Could not open Database")
|
||||
}
|
||||
|
||||
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)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
|
||||
go func() {
|
||||
for range c {
|
||||
Close(&db)
|
||||
Close(db)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
err := open(fmt.Sprintf("http://localhost:%d", port))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}()
|
||||
|
||||
err = s.Start(port)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
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,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,10 +170,11 @@ func open(url string) error {
|
||||
}
|
||||
|
||||
func Close(db *database.Manager) {
|
||||
fmt.Println("Attempting to save and close DB")
|
||||
log.Debug().Msg("Closing Database")
|
||||
err := db.Close()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
log.Error().Err(err).Msg("Could not close Database")
|
||||
return
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
BIN
mangaGetter
BIN
mangaGetter
Binary file not shown.
26
release.go
26
release.go
@@ -7,7 +7,31 @@ import (
|
||||
"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 {
|
||||
dir, err := os.UserCacheDir()
|
||||
|
||||
Reference in New Issue
Block a user