Changed Project structure, added Hot Reload of gohtml files for Develop and embedding of gohtml files for Release

This commit is contained in:
Pablu23
2024-02-23 10:59:26 +01:00
parent cc7009c106
commit 9bc483afb3
13 changed files with 196 additions and 143 deletions

View File

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

View File

@@ -0,0 +1,174 @@
package database
import (
"database/sql"
_ "embed"
"sync"
_ "github.com/mattn/go-sqlite3"
)
type Manga struct {
Id int
Title string
TimeStampUnix int64
// Not in DB
LatestChapter *Chapter
}
type Chapter struct {
Id int
Manga *Manga
Url string
Name string
Number int
TimeStampUnix int64
}
type DatabaseManager struct {
ConnectionString string
db *sql.DB
Rw *sync.Mutex
Mangas map[int]*Manga
Chapters map[int]*Chapter
CreateIfNotExists bool
}
func NewDatabase(connectionString string, createIfNotExists bool) DatabaseManager {
return DatabaseManager{
ConnectionString: connectionString,
Rw: &sync.Mutex{},
Mangas: make(map[int]*Manga),
Chapters: make(map[int]*Chapter),
CreateIfNotExists: createIfNotExists,
}
}
func (dbMgr *DatabaseManager) Open() error {
db, err := sql.Open("sqlite3", dbMgr.ConnectionString)
if err != nil {
return err
}
dbMgr.db = db
if dbMgr.CreateIfNotExists {
err = dbMgr.createDatabaseIfNotExists()
if err != nil {
return err
}
}
err = dbMgr.load()
return err
}
func (dbMgr *DatabaseManager) Close() error {
err := dbMgr.db.Close()
if err != nil {
return err
}
dbMgr.Mangas = nil
dbMgr.Chapters = nil
dbMgr.db = nil
return nil
}
func (dbMgr *DatabaseManager) Save() error {
db := dbMgr.db
dbMgr.Rw.Lock()
defer dbMgr.Rw.Unlock()
for _, m := range dbMgr.Mangas {
count := 0
err := db.QueryRow("SELECT COUNT(*) FROM Manga where ID = ?", m.Id).Scan(&count)
if err != nil {
return err
}
if count == 0 {
_, err := db.Exec("INSERT INTO Manga(ID, Title, TimeStampUnixEpoch) values(?, ?, ?)", m.Id, m.Title, m.TimeStampUnix)
if err != nil {
return err
}
} else {
_, err := db.Exec("UPDATE Manga set Title = ?, TimeStampUnixEpoch = ? WHERE ID = ?", m.Title, m.TimeStampUnix, m.Id)
if err != nil {
return err
}
}
}
for _, c := range dbMgr.Chapters {
count := 0
err := db.QueryRow("SELECT COUNT(*) FROM Chapter where ID = ?", c.Id).Scan(&count)
if err != nil {
return err
}
if count == 0 {
_, err := db.Exec("INSERT INTO Chapter(ID, MangaID, Url, Name, Number, TimeStampUnixEpoch) VALUES (?, ?, ?, ?, ?, ?)", c.Id, c.Manga.Id, c.Url, c.Name, c.Number, c.TimeStampUnix)
if err != nil {
return err
}
} else {
_, err = db.Exec("UPDATE Chapter set Name = ?, Url = ?, Number = ?, TimeStampUnixEpoch = ? where ID = ?", c.Name, c.Url, c.Number, c.TimeStampUnix, c.Id)
if err != nil {
return err
}
}
}
return nil
}
//go:embed createDb.sql
var createSql string
func (dbMgr *DatabaseManager) createDatabaseIfNotExists() error {
_, err := dbMgr.db.Exec(createSql)
return err
}
func (dbMgr *DatabaseManager) load() error {
db := dbMgr.db
dbMgr.Rw.Lock()
defer dbMgr.Rw.Unlock()
rows, err := db.Query("SELECT * FROM Manga")
if err != nil {
return err
}
for rows.Next() {
manga := Manga{}
if err = rows.Scan(&manga.Id, &manga.Title, &manga.TimeStampUnix); err != nil {
return err
}
dbMgr.Mangas[manga.Id] = &manga
}
rows, err = db.Query("SELECT * FROM Chapter")
if err != nil {
return err
}
for rows.Next() {
chapter := Chapter{}
var mangaID int
if err = rows.Scan(&chapter.Id, &mangaID, &chapter.Url, &chapter.Name, &chapter.Number, &chapter.TimeStampUnix); err != nil {
return err
}
chapter.Manga = dbMgr.Mangas[mangaID]
if dbMgr.Mangas[mangaID].LatestChapter == nil || dbMgr.Mangas[mangaID].LatestChapter.TimeStampUnix < chapter.TimeStampUnix {
dbMgr.Mangas[mangaID].LatestChapter = &chapter
}
dbMgr.Chapters[chapter.Id] = &chapter
}
return nil
}

110
internal/provider/bato.go Normal file
View File

@@ -0,0 +1,110 @@
package provider
import (
"errors"
"fmt"
"io"
"net/http"
"regexp"
"strconv"
)
type Bato struct{}
func (b *Bato) GetImageList(html string) ([]string, error) {
reg, err := regexp.Compile(`<astro-island.*props=".*;imageFiles&quot;:\[1,&quot;\[(.*)]&quot;]`)
if err != nil {
return nil, err
}
m := reg.FindStringSubmatch(html)
if len(m) <= 0 {
return nil, errors.New("no more content")
}
match := m[1]
reg, err = regexp.Compile(`\[0,\\&quot;([^&]*)\\&quot;]`)
if err != nil {
return nil, err
}
matches := reg.FindAllStringSubmatch(match, -1)
l := len(matches)
result := make([]string, l)
for i, m := range matches {
result[i] = m[1]
}
return result, nil
}
func (b *Bato) GetHtml(titleSubUrl string) (string, error) {
url := fmt.Sprintf("https://bato.to%s?load=2", titleSubUrl)
resp, err := http.Get(url)
// TODO: Testing for above 300 is dirty
if err != nil && resp.StatusCode > 300 {
return "", errors.New("could not get html")
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
fmt.Printf("Could not close body because: %v\n", err)
}
}(resp.Body)
all, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
h := string(all)
return h, nil
}
func (b *Bato) GetNext(html string) (subUrl string, err error) {
reg, err := regexp.Compile(`<a data-hk="0-6-0" .*? href="(.*?)["']`)
match := reg.FindStringSubmatch(html)
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)
return match[1], err
}
func (b *Bato) GetTitleAndChapter(url string) (title string, chapter string, err error) {
reg, err := regexp.Compile(`/title/\d*-(.*?)/\d*-(.*)`)
if err != nil {
return "", "", err
}
matches := reg.FindAllStringSubmatch(url, -1)
if len(matches) <= 0 {
return "", "", errors.New("no title or chapter found")
}
return matches[0][1], matches[0][2], nil
}
func (b *Bato) GetTitleIdAndChapterId(url string) (titleId int, chapterId int, err error) {
reg, err := regexp.Compile(`/title/(\d*)-.*?/(\d*)-.*`)
if err != nil {
return 0, 0, err
}
matches := reg.FindAllStringSubmatch(url, -1)
if len(matches) <= 0 {
return 0, 0, errors.New("no title or chapter found")
}
t, err := strconv.Atoi(matches[0][1])
if err != nil {
return 0, 0, err
}
c, err := strconv.Atoi(matches[0][2])
return t, c, err
}

View File

@@ -0,0 +1,10 @@
package provider
type Provider interface {
GetImageList(html string) (imageUrls []string, err error)
GetHtml(url string) (html string, err error)
GetNext(html string) (url string, err error)
GetPrev(html string) (url string, err error)
GetTitleAndChapter(url string) (title string, chapter string, err error)
GetTitleIdAndChapterId(url string) (titleId int, chapterId int, err error)
}

380
internal/server/server.go Normal file
View File

@@ -0,0 +1,380 @@
package server
import (
"bytes"
"fmt"
"golang.org/x/text/language"
"html/template"
"io"
"mangaGetter/internal/database"
"mangaGetter/internal/provider"
"mangaGetter/internal/view"
"net/http"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"golang.org/x/text/cases"
)
type Server struct {
PrevViewModel *view.ImageViewModel
CurrViewModel *view.ImageViewModel
NextViewModel *view.ImageViewModel
ImageBuffers map[string]*bytes.Buffer
Mutex *sync.Mutex
NextSubUrl string
CurrSubUrl string
PrevSubUrl string
Provider provider.Provider
IsFirst bool
IsLast bool
DbMgr *database.DatabaseManager
// I'm not even sure if this helps.
// If you press next and then prev too fast you still lock yourself out
NextReady chan bool
PrevReady chan bool
}
func (s *Server) HandleImage(w http.ResponseWriter, r *http.Request) {
u := r.PathValue("url")
s.Mutex.Lock()
buf := s.ImageBuffers[u]
if buf == nil {
fmt.Printf("url: %s is nil\n", u)
w.WriteHeader(400)
return
}
w.Header().Set("Content-Type", "image/webp")
_, err := w.Write(buf.Bytes())
if err != nil {
fmt.Println(err)
}
s.Mutex.Unlock()
}
func (s *Server) HandleNext(w http.ResponseWriter, r *http.Request) {
fmt.Println("Received Next")
if s.PrevViewModel != nil {
go func(viewModel view.ImageViewModel, s *Server) {
s.Mutex.Lock()
for _, img := range viewModel.Images {
delete(s.ImageBuffers, img.Path)
}
s.Mutex.Unlock()
fmt.Println("Cleaned out of scope Last")
}(*s.PrevViewModel, s)
}
s.PrevViewModel = s.CurrViewModel
s.CurrViewModel = s.NextViewModel
s.PrevSubUrl = s.CurrSubUrl
s.CurrSubUrl = s.NextSubUrl
<-s.NextReady
go s.LoadNext()
http.Redirect(w, r, "/current/", http.StatusTemporaryRedirect)
}
func (s *Server) LoadNext() {
c, err := s.Provider.GetHtml(s.CurrSubUrl)
if err != nil {
fmt.Println(err)
return
}
next, err := s.Provider.GetNext(c)
if err != nil {
fmt.Println(err)
return
}
html, err := s.Provider.GetHtml(next)
if err != nil {
fmt.Println(err)
return
}
imagesNext, err := s.AppendImagesToBuf(html)
if err != nil {
fmt.Println(err)
return
}
title, chapter, err := s.Provider.GetTitleAndChapter(next)
if err != nil {
title = "Unknown"
chapter = "ch_?"
}
full := strings.Replace(title, "-", " ", -1) + " - " + strings.Replace(chapter, "_", " ", -1)
s.NextViewModel = &view.ImageViewModel{Images: imagesNext, Title: full}
s.NextSubUrl = next
fmt.Println("Loaded next")
s.NextReady <- true
}
func (s *Server) LoadPrev() {
c, err := s.Provider.GetHtml(s.CurrSubUrl)
if err != nil {
fmt.Println(err)
return
}
prev, err := s.Provider.GetPrev(c)
if err != nil {
fmt.Println(err)
return
}
html, err := s.Provider.GetHtml(prev)
if err != nil {
fmt.Println(err)
return
}
imagesNext, err := s.AppendImagesToBuf(html)
if err != nil {
fmt.Println(err)
return
}
title, chapter, err := s.Provider.GetTitleAndChapter(prev)
if err != nil {
title = "Unknown"
chapter = "ch_?"
}
full := strings.Replace(title, "-", " ", -1) + " - " + strings.Replace(chapter, "_", " ", -1)
s.PrevViewModel = &view.ImageViewModel{Images: imagesNext, Title: full}
s.PrevSubUrl = prev
fmt.Println("Loaded prev")
s.PrevReady <- true
}
func (s *Server) HandlePrev(w http.ResponseWriter, r *http.Request) {
fmt.Println("Received Prev")
if s.NextViewModel != nil {
go func(viewModel view.ImageViewModel, s *Server) {
s.Mutex.Lock()
for _, img := range viewModel.Images {
delete(s.ImageBuffers, img.Path)
}
s.Mutex.Unlock()
fmt.Println("Cleaned out of scope Last")
}(*s.NextViewModel, s)
}
s.NextViewModel = s.CurrViewModel
s.CurrViewModel = s.PrevViewModel
s.NextSubUrl = s.CurrSubUrl
s.CurrSubUrl = s.PrevSubUrl
<-s.PrevReady
go s.LoadPrev()
http.Redirect(w, r, "/current/", http.StatusTemporaryRedirect)
}
func (s *Server) HandleCurrent(w http.ResponseWriter, _ *http.Request) {
tmpl := template.Must(view.GetViewTemplate(view.Viewer))
s.DbMgr.Rw.Lock()
defer s.DbMgr.Rw.Unlock()
mangaId, chapterId, err := s.Provider.GetTitleIdAndChapterId(s.CurrSubUrl)
if err != nil {
fmt.Println(err)
} else {
title, chapter, err := s.Provider.GetTitleAndChapter(s.CurrSubUrl)
if err != nil {
fmt.Println(err)
} else {
var manga *database.Manga
if s.DbMgr.Mangas[mangaId] == nil {
manga = &database.Manga{
Id: mangaId,
Title: title,
TimeStampUnix: time.Now().Unix(),
}
s.DbMgr.Mangas[mangaId] = manga
} else {
manga = s.DbMgr.Mangas[mangaId]
s.DbMgr.Mangas[mangaId].TimeStampUnix = time.Now().Unix()
}
if s.DbMgr.Chapters[chapterId] == nil {
chapterNumberStr := strings.Replace(chapter, "ch_", "", 1)
number, err := strconv.Atoi(chapterNumberStr)
if err != nil {
fmt.Println(err)
number = 0
}
s.DbMgr.Chapters[chapterId] = &database.Chapter{
Id: chapterId,
Manga: manga,
Url: s.CurrSubUrl,
Name: chapter,
Number: number,
TimeStampUnix: time.Now().Unix(),
}
} else {
s.DbMgr.Chapters[chapterId].TimeStampUnix = time.Now().Unix()
}
}
}
err = tmpl.Execute(w, s.CurrViewModel)
if err != nil {
fmt.Println(err)
}
}
func (s *Server) HandleNew(w http.ResponseWriter, r *http.Request) {
title := r.PathValue("title")
chapter := r.PathValue("chapter")
url := fmt.Sprintf("/title/%s/%s", title, chapter)
s.Mutex.Lock()
s.ImageBuffers = make(map[string]*bytes.Buffer)
s.Mutex.Unlock()
s.CurrSubUrl = url
s.PrevSubUrl = ""
s.NextSubUrl = ""
s.LoadCurr()
go s.LoadNext()
go s.LoadPrev()
http.Redirect(w, r, "/current/", http.StatusTemporaryRedirect)
}
func (s *Server) LoadCurr() {
html, err := s.Provider.GetHtml(s.CurrSubUrl)
if err != nil {
panic(err)
}
imagesCurr, err := s.AppendImagesToBuf(html)
title, chapter, err := s.Provider.GetTitleAndChapter(s.CurrSubUrl)
if err != nil {
title = "Unknown"
chapter = "ch_?"
}
full := strings.Replace(title, "-", " ", -1) + " - " + strings.Replace(chapter, "_", " ", -1)
s.CurrViewModel = &view.ImageViewModel{Images: imagesCurr, Title: full}
fmt.Println("Loaded current")
}
func (s *Server) AppendImagesToBuf(html string) ([]view.Image, error) {
imgList, err := s.Provider.GetImageList(html)
if err != nil {
return nil, err
}
images := make([]view.Image, len(imgList))
wg := sync.WaitGroup{}
for i, url := range imgList {
wg.Add(1)
go func(i int, url string, wg *sync.WaitGroup) {
buf, err := addFileToRam(url)
if err != nil {
panic(err)
}
name := filepath.Base(url)
s.Mutex.Lock()
s.ImageBuffers[name] = buf
s.Mutex.Unlock()
images[i] = view.Image{Path: name, Index: i}
wg.Done()
}(i, url, &wg)
}
wg.Wait()
return images, nil
}
func (s *Server) HandleMenu(w http.ResponseWriter, _ *http.Request) {
tmpl := template.Must(view.GetViewTemplate(view.Menu))
s.DbMgr.Rw.Lock()
defer s.DbMgr.Rw.Unlock()
all := s.DbMgr.Mangas
l := len(all)
mangaViewModels := make([]view.MangaViewModel, l)
counter := 0
for _, manga := range all {
title := cases.Title(language.English, cases.Compact).String(strings.Replace(manga.Title, "-", " ", -1))
mangaViewModels[counter] = view.MangaViewModel{
Title: title,
Number: manga.LatestChapter.Number,
// I Hate this time Format... 15 = hh, 04 = mm, 02 = DD, 01 = MM, 06 == YY
LastTime: time.Unix(manga.TimeStampUnix, 0).Format("15:04 (02-01-06)"),
Url: manga.LatestChapter.Url,
}
counter++
}
menuViewModel := view.MenuViewModel{
Mangas: mangaViewModels,
}
err := tmpl.Execute(w, menuViewModel)
if err != nil {
fmt.Println(err)
}
}
func (s *Server) HandleExit(w http.ResponseWriter, r *http.Request) {
err := s.DbMgr.Save()
if err != nil {
fmt.Println(err)
return
}
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
}
func addFileToRam(url string) (*bytes.Buffer, error) {
// Get the data
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
fmt.Println(err)
}
}(resp.Body)
buf := new(bytes.Buffer)
// Write the body to file
_, err = io.Copy(buf, resp.Body)
return buf, err
}

View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Main Menu</title>
</head>
<body>
<form method="post" action="/new/">
</form>
{{range .Mangas}}
<div>
<text>Title: {{.Title}}</text>
<text>Current Chapter: {{.Number}}</text>
<text>Last Accessed: {{.LastTime}}</text>
<a href="/new/{{.Url}}}" class="button">Go to last Chapter</a>
</div>
{{end}}
</body>
</html>

View File

@@ -0,0 +1,103 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{.Title}}</title>
<style>
body {background-color: #171717}
.scroll-container {
overflow: auto;
margin-left: auto;
margin-right: auto;
padding-bottom: 0;
margin-bottom: 0;
width: 100%;
display: block;
box-sizing: border-box;
border-width: 0;
}
.scroll-container img {
padding: 0;
display: block;
/*min-width: 20%;*/
margin-left: auto;
margin-right: auto;
max-width: 100%;
vertical-align: middle;
}
/*
* I have no clue what css is, jesus christ ...
*/
.center {
margin-left: auto;
margin-right: auto;
width: 50%;
padding: 10px;
vertical-align: middle;
justify-content: center;
display: flex;
}
.button-36 {
background-image: linear-gradient(92.88deg, #455EB5 9.16%, #5643CC 43.89%, #673FD7 64.72%);
border-radius: 8px;
border-style: none;
box-sizing: border-box;
color: #FFFFFF;
cursor: pointer;
flex-shrink: 0;
font-family: "Inter UI","SF Pro Display",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Open Sans","Helvetica Neue",sans-serif;
font-size: 16px;
font-weight: 500;
height: 4rem;
padding: 0 1.6rem;
text-align: center;
text-shadow: rgba(0, 0, 0, 0.25) 0 3px 8px;
transition: all .5s;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
}
.button-36:hover {
box-shadow: rgba(80, 63, 205, 0.5) 0 1px 30px;
transition-duration: .1s;
}
@media (min-width: 768px) {
.button-36 {
padding: 0 2.6rem;
}
}
</style>
</head>
<body>
<div class="center">
<form method="post" action="/prev">
<input type="submit" name="Prev" value="Prev" class="button-36">
<input type="submit" name="Exit" value="Exit" class="button-36" formaction="/exit">
<input type="submit" name="Next" value="Next" class="button-36" formaction="/next">
</form>
</div>
<div class="scroll-container">
{{range .Images}}
<img src="/img/{{.Path}}" alt="img_{{.Index}}"/>
{{end}}
</div>
<div class="center">
<form method="post" action="/prev">
<input type="submit" name="Prev" value="Prev" class="button-36">
<input type="submit" name="Exit" value="Exit" class="button-36" formaction="/exit">
<input type="submit" name="Next" value="Next" class="button-36" formaction="/next">
</form>
</div>
</body>
</html>

25
internal/view/embedded.go Normal file
View File

@@ -0,0 +1,25 @@
//go:build !Develop
package view
import (
_ "embed"
"errors"
"html/template"
)
//go:embed Views/menu.gohtml
var menu string
//go:embed Views/viewer.gohtml
var viewer 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)
}
return nil, errors.New("invalid view")
}

View File

@@ -0,0 +1,18 @@
//go:build Develop
package view
import (
"html/template"
)
func GetViewTemplate(view View) (*template.Template, error) {
var path string
switch view {
case Menu:
path = "internal/view/Views/menu.gohtml"
case Viewer:
path = "internal/view/Views/viewer.gohtml"
}
return template.ParseFiles(path)
}

View File

@@ -0,0 +1,22 @@
package view
type Image struct {
Path string
Index int
}
type ImageViewModel struct {
Title string
Images []Image
}
type MangaViewModel struct {
Title string
Number int
LastTime string
Url string
}
type MenuViewModel struct {
Mangas []MangaViewModel
}

8
internal/view/views.go Normal file
View File

@@ -0,0 +1,8 @@
package view
type View int
const (
Menu View = iota
Viewer View = iota
)