18 Commits

Author SHA1 Message Date
Pablu23
c1fa18fead WIP 2024-05-21 15:17:29 +02:00
Pablu23
20ad56b155 Initial test, but meh 2024-05-21 11:56:14 +02:00
Pablu23
0904a1214e Changed makefile to reflect Project structure changes 2024-05-18 14:25:24 +02:00
Pablu23
bf4b4a3a15 Changed Project structure for non Package Project 2024-05-18 14:21:56 +02:00
Pablu23
1bd80bc1c2 Change own DbTable implementation for Gorm 2024-05-14 11:28:49 +02:00
Pablu23
620a043286 minor changes to table.go 2024-04-04 21:36:12 +02:00
Pablu23
56fc884952 Changed module name to github Path, and minor changes 2024-04-04 09:55:29 +02:00
Pablu23
58344d4def Fixed various Bugs 2024-04-03 16:12:44 +02:00
Pablu23
cb592c7109 Added Settings for Manga ordering, fixed theme selector 2024-04-03 13:13:08 +02:00
Pablu23
f712211194 Added different Ports for release and develop, changed out Sort for simple iteration (Should be faster), Fixed Theme Switcher, fixed Saving of read chapters 2024-04-02 19:27:25 +02:00
Pablu23
e3e0da86fc Supress Theme warning 2024-04-02 19:06:13 +02:00
Pablu23
e45109fcd5 Added initial Settings 2024-04-02 19:05:36 +02:00
Pablu23
c83a10823d Significantly simplified adding new Database Tables 2024-04-02 18:22:54 +02:00
Pablu23
ce878efce3 Added start of db rework, also added Qol changes 2024-04-02 16:35:50 +02:00
Pablu23
1377fd420e Added saving of LatestAvailableChapter and updating it in goroutine every 5 Minutes, also Added filtering to menu 2024-03-06 23:27:14 +01:00
Pablu23
ad1fcbc68a Now actually use Thumbnail that was loaded from db, instead of re downloading it every time. Also save Thumbnail to Manga if it couldnt be saved before 2024-03-01 16:46:19 +01:00
Pablu23
b7f2a389cd Added chaching, and significantly improved Menu loading times after first 2024-03-01 14:54:25 +01:00
Pablu23
a8ba8728dc Added Last Chapter functionality to see how many more chapters you have, also kinda works as update Status bar
changed it to only load latest chapter instead of all chapters for manga
Added develop and release db from different locations
Added stopwatch for future performance improvement metrics
2024-03-01 14:31:38 +01:00
26 changed files with 884 additions and 676 deletions

5
.gitignore vendored
View File

@@ -2,4 +2,7 @@
findings.txt
test.txt
h.html
db.sqlite
*.sqlite
*.bak
/bin
*.exe

View File

@@ -2,10 +2,8 @@ run: develop
bin/develop
develop:
go build -tags Develop -o bin/develop cmd/mangaGetter/main.go
go build -tags Develop -o bin/develop
release:
go build -o bin/MangaGetter_unix cmd/mangaGetter/main.go
go build -o bin/MangaGetter_unix
win-amd64:
GOOS=windows GOARCH=amd64 go build -o bin/MangaGetter-amd64_windows.exe cmd/mangaGetter/main.go
GOOS=windows GOARCH=amd64 go build -o bin/MangaGetter-amd64_windows.exe

View File

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

View File

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

9
develop.go Normal file
View File

@@ -0,0 +1,9 @@
//go:build Develop
package main
const port = 8080
func getDbPath() string {
return "db.sqlite"
}

9
go.mod
View File

@@ -1,4 +1,4 @@
module mangaGetter
module github.com/pablu23/mangaGetter
go 1.22
@@ -7,4 +7,9 @@ require (
golang.org/x/text v0.14.0
)
require github.com/google/uuid v1.6.0 // indirect
require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
gorm.io/driver/sqlite v1.5.5
gorm.io/gorm v1.25.10
)

10
go.sum
View File

@@ -1,6 +1,12 @@
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/mattn/go-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=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E=
gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE=
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=

View File

@@ -1,11 +1,24 @@
package database
type Chapter struct {
Id int
Manga *Manga
InternalIdentifier string
Id int `gorm:"primary_key;autoIncrement;"`
ChapterId int
Url string
Name string
Number int
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 {
return Chapter{
ChapterId: id,
Url: url,
Name: name,
Number: number,
TimeStampUnix: timeStampUnix,
MangaId: mangaId,
UserId: userId,
}
}

View File

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

View File

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

View File

@@ -1,18 +1,65 @@
package database
import (
"bytes"
"mangaGetter/internal/provider"
)
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
Provider provider.Provider
Rating int
Title string
Id int `gorm:"primary_key;AUTO_INCREMENT"`
MangaDefinitionId int
Definition MangaDefinition `gorm:"foreignKey:MangaDefinitionId"`
UserId int
User User
TimeStampUnix int64
Thumbnail *bytes.Buffer
// Not in DB
LatestChapter *Chapter
Chapters []Chapter `gorm:"foreignKey:MangaId"`
}
func NewMangaDefinition(id int, title string) MangaDefinition {
return MangaDefinition{
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,
}
}
// 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
// }
// }
// if highest == 0 {
return nil, false
// }
// return &m.Chapters[index], true
//result := db.Where("manga.id = ?", m.Id).Order("TimeStampUnix desc").Take(&chapter)
//if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
// return &chapter, true, result.Error
//} else if errors.Is(result.Error, gorm.ErrRecordNotFound) {
// return &chapter, false, nil
//} else {
// return &chapter, true, nil
//}
}

View File

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

17
internal/database/user.go Normal file
View File

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

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

View File

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

View File

@@ -109,9 +109,24 @@ func (b *Bato) GetTitleIdAndChapterId(url string) (titleId int, chapterId int, e
return t, c, err
}
//func (b *Bato) GetChapterList(url string) (chapterIds []int, err error) {
//
//}
func (b *Bato) GetChapterList(subUrl string) (subUrls []string, err error) {
reg, err := regexp.Compile(`<div class="space-x-1">.*?<a href="(.*?)" .*?>.*?</a>`)
if err != nil {
return nil, err
}
html, err := b.GetHtml(subUrl)
if err != nil {
return nil, err
}
subUrls = make([]string, 0)
matches := reg.FindAllStringSubmatch(html, -1)
for _, match := range matches {
subUrls = append(subUrls, match[1])
}
return subUrls, nil
}
func (b *Bato) GetThumbnail(subUrl string) (thumbnailUrl string, err error) {
url := fmt.Sprintf("https://bato.to/title/%s", subUrl)

View File

@@ -8,4 +8,5 @@ type Provider interface {
GetTitleAndChapter(url string) (title string, chapter string, err error)
GetTitleIdAndChapterId(url string) (titleId int, chapterId int, err error)
GetThumbnail(mangaId string) (thumbnailUrl string, err error)
GetChapterList(url string) (urls []string, err error)
}

View File

@@ -1,67 +1,226 @@
package server
import (
"bytes"
"cmp"
_ "embed"
"errors"
"fmt"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"html/template"
"mangaGetter/internal/database"
"mangaGetter/internal/view"
"net/http"
"slices"
"strconv"
"strings"
"time"
"github.com/pablu23/mangaGetter/internal/database"
"github.com/pablu23/mangaGetter/internal/view"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"gorm.io/gorm"
)
func (s *Server) HandleMenu(w http.ResponseWriter, _ *http.Request) {
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) HandleRegister(w http.ResponseWriter, r *http.Request) {
s.Mutex.Lock()
defer s.Mutex.Unlock()
admin := database.User{
Id: 1,
DisplayName: "admin",
LoginName: "admin",
}
s.DbMgr.Db.Create(&admin)
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
}
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)
}
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)
session, err := s.getSessionFromCookie(w, r)
if err != nil {
return
}
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))
fmt.Println("Locking Rw in handler.go:43")
s.DbMgr.Rw.Lock()
defer func() {
fmt.Println("Unlocking Rw in handler.go:46")
s.DbMgr.Rw.Unlock()
}()
all := s.DbMgr.Mangas
session, err := s.getSessionFromCookie(w, r)
if err != nil {
return
}
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
for _, manga := range all {
title := cases.Title(language.English, cases.Compact).String(strings.Replace(manga.Title, "-", " ", -1))
n := time.Now().UnixNano()
thumbnail, err := s.LoadThumbnail(manga.Id)
var tmp []database.Setting
s.DbMgr.Db.Find(&tmp)
settings := make(map[string]database.Setting)
for _, m := range tmp {
settings[m.Name] = m
}
var thumbNs int64 = 0
var titNs int64 = 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()
thumbnail, updated, err := s.LoadThumbnail(manga)
//TODO: Add default picture instead of not showing Manga at all
if err != nil {
continue
}
manga.Thumbnail = s.ImageBuffers[thumbnail]
if updated {
s.DbMgr.Db.Save(manga)
}
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 == "" {
err, updated := s.UpdateLatestAvailableChapter(manga)
if err != nil {
fmt.Println(err)
}
if updated {
s.DbMgr.Db.Save(manga.Definition)
}
}
t2 = time.Now().UnixNano()
titNs += t2 - t1
latestChapter, ok := manga.GetLatestChapter()
if !ok {
continue
}
mangaViewModels[counter] = view.MangaViewModel{
ID: manga.Id,
ID: manga.Definition.Id,
Title: title,
Number: manga.LatestChapter.Number,
Number: latestChapter.Number,
LastNumber: manga.Definition.LastChapterNum,
// I Hate this time Format... 15 = hh, 04 = mm, 02 = DD, 01 = MM, 06 == YY
LastTime: time.Unix(manga.TimeStampUnix, 0).Format("15:04 (02-01-06)"),
Url: manga.LatestChapter.Url,
Url: latestChapter.Url,
ThumbnailUrl: thumbnail,
}
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 {
return cmp.Compare(a.Title, b.Title)
})
} else if order.Value == "chapter" {
slices.SortStableFunc(mangaViewModels, func(a, b view.MangaViewModel) int {
return cmp.Compare(b.Number, a.Number)
})
} else if order.Value == "last" {
slices.SortStableFunc(mangaViewModels, func(a, b view.MangaViewModel) int {
aT, err := time.Parse("15:04 (02-01-06)", a.LastTime)
if err != nil {
return cmp.Compare(a.Title, b.Title)
}
bT, err := time.Parse("15:04 (02-01-06)", b.LastTime)
if err != nil {
return cmp.Compare(a.Title, b.Title)
}
return bT.Compare(aT)
})
}
nex = time.Now().UnixNano()
fmt.Printf("Sorting took %d ms\n", (nex-n)/1000000)
menuViewModel := view.MenuViewModel{
Settings: settings,
Mangas: mangaViewModels,
}
err := tmpl.Execute(w, menuViewModel)
err = tmpl.Execute(w, menuViewModel)
if err != nil {
fmt.Println(err)
}
@@ -82,80 +241,85 @@ func (s *Server) HandleDelete(w http.ResponseWriter, r *http.Request) {
return
}
err = s.DbMgr.Delete(mangaId)
if err != nil {
fmt.Println(err)
}
s.DbMgr.Delete(mangaId)
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
}
func (s *Server) HandleExit(w http.ResponseWriter, r *http.Request) {
err := s.DbMgr.Save()
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
// session, err := s.getSessionFromCookie(w, r)
// if err != nil {
// return
// }
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")
}()
}
func (s *Server) HandleCurrent(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(view.GetViewTemplate(view.Viewer))
session, err := s.getSessionFromCookie(w, r)
if err != nil {
fmt.Println(err)
return
}
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
}
func (s *Server) HandleCurrent(w http.ResponseWriter, _ *http.Request) {
tmpl := template.Must(view.GetViewTemplate(view.Viewer))
fmt.Println("Locking Rw in handler.go:125")
s.DbMgr.Rw.Lock()
defer func() {
fmt.Println("Unlocking Rw in handler.go:128")
s.DbMgr.Rw.Unlock()
}()
mangaId, chapterId, err := s.Provider.GetTitleIdAndChapterId(s.CurrSubUrl)
mangaId, chapterId, err := s.Provider.GetTitleIdAndChapterId(session.CurrSubUrl)
if err != nil {
fmt.Println(err)
} else {
title, chapter, err := s.Provider.GetTitleAndChapter(s.CurrSubUrl)
}
title, chapterName, err := s.Provider.GetTitleAndChapter(session.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)
}
var manga database.Manga
result = s.DbMgr.Db.Where("user_id = ?", session.User.Id).First(&manga, "manga_definition_id = ?", mangaId)
if result.Error != nil && errors.Is(result.Error, gorm.ErrRecordNotFound) {
manga = database.NewManga(mangaDef, session.User, time.Now().Unix())
}
var chapter database.Chapter
result = s.DbMgr.Db.Where("user_id = ?", session.User.Id).First(&chapter, "chapter_id = ?", 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())
} 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()
chapter.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.Db.Save(&mangaDef)
s.DbMgr.Db.Save(&manga)
s.DbMgr.Db.Save(&chapter)
s.DbMgr.Chapters[chapterId] = &database.Chapter{
Id: chapterId,
Manga: manga,
Url: s.CurrSubUrl,
Name: chapter,
Number: number,
TimeStampUnix: time.Now().Unix(),
}
} else {
s.DbMgr.Chapters[chapterId].TimeStampUnix = time.Now().Unix()
}
s.DbMgr.Mangas[mangaId].LatestChapter = s.DbMgr.Chapters[chapterId]
}
}
err = tmpl.Execute(w, s.CurrViewModel)
err = tmpl.Execute(w, session.CurrViewModel)
if err != nil {
fmt.Println(err)
}
@@ -163,8 +327,8 @@ func (s *Server) HandleCurrent(w http.ResponseWriter, _ *http.Request) {
func (s *Server) HandleImage(w http.ResponseWriter, r *http.Request) {
u := r.PathValue("url")
s.Mutex.Lock()
defer s.Mutex.Unlock()
s.Mutex.RLock()
defer s.Mutex.RUnlock()
buf := s.ImageBuffers[u]
if buf == nil {
fmt.Printf("url: %s is nil\n", u)
@@ -173,7 +337,7 @@ func (s *Server) HandleImage(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "image/webp")
_, err := w.Write(buf.Bytes())
_, err := w.Write(buf)
if err != nil {
fmt.Println(err)
}
@@ -182,7 +346,7 @@ func (s *Server) HandleImage(w http.ResponseWriter, r *http.Request) {
//go:embed favicon.ico
var ico []byte
func (s *Server) HandleFavicon(w http.ResponseWriter, r *http.Request) {
func (s *Server) HandleFavicon(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "image/webp")
_, err := w.Write(ico)
if err != nil {
@@ -193,7 +357,12 @@ func (s *Server) HandleFavicon(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) {
s.Mutex.Lock()
for _, img := range viewModel.Images {
@@ -201,30 +370,31 @@ func (s *Server) HandleNext(w http.ResponseWriter, r *http.Request) {
}
s.Mutex.Unlock()
fmt.Println("Cleaned out of scope Last")
}(*s.PrevViewModel, s)
}(*session.PrevViewModel, s)
}
if s.NextViewModel == nil || s.NextSubUrl == "" {
if session.NextViewModel == nil || session.NextSubUrl == "" {
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
err := s.DbMgr.Save()
if err != nil {
fmt.Println(err)
}
return
}
s.PrevViewModel = s.CurrViewModel
s.CurrViewModel = s.NextViewModel
s.PrevSubUrl = s.CurrSubUrl
s.CurrSubUrl = s.NextSubUrl
session.PrevViewModel = session.CurrViewModel
session.CurrViewModel = session.NextViewModel
session.PrevSubUrl = session.CurrSubUrl
session.CurrSubUrl = session.NextSubUrl
go s.LoadNext()
go s.LoadNext(session)
http.Redirect(w, r, "/current/", http.StatusTemporaryRedirect)
}
func (s *Server) HandlePrev(w http.ResponseWriter, r *http.Request) {
fmt.Println("Received Prev")
if s.NextViewModel != nil {
session, err := s.getSessionFromCookie(w, r)
if err != nil {
return
}
if session.NextViewModel != nil {
go func(viewModel view.ImageViewModel, s *Server) {
s.Mutex.Lock()
for _, img := range viewModel.Images {
@@ -232,41 +402,74 @@ func (s *Server) HandlePrev(w http.ResponseWriter, r *http.Request) {
}
s.Mutex.Unlock()
fmt.Println("Cleaned out of scope Last")
}(*s.NextViewModel, s)
}(*session.NextViewModel, s)
}
if s.PrevViewModel == nil || s.PrevSubUrl == "" {
if session.PrevViewModel == nil || session.PrevSubUrl == "" {
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
err := s.DbMgr.Save()
if err != nil {
fmt.Println(err)
}
return
}
s.NextViewModel = s.CurrViewModel
s.CurrViewModel = s.PrevViewModel
s.NextSubUrl = s.CurrSubUrl
s.CurrSubUrl = s.PrevSubUrl
session.NextViewModel = session.CurrViewModel
session.CurrViewModel = session.PrevViewModel
session.NextSubUrl = session.CurrSubUrl
session.CurrSubUrl = session.PrevSubUrl
go s.LoadPrev()
go s.LoadPrev(session)
http.Redirect(w, r, "/current/", http.StatusTemporaryRedirect)
}
func (s *Server) HandleSettingSet(w http.ResponseWriter, r *http.Request) {
settingName := r.PathValue("setting")
settingValue := r.PathValue("value")
var setting database.Setting
res := s.DbMgr.Db.First(&setting, "name = ?", settingName)
if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) {
set := database.NewSetting(settingName, settingValue)
s.DbMgr.Db.Save(&set)
} else {
s.DbMgr.Db.Model(&setting).Update("value", settingValue)
}
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
}
func (s *Server) HandleSetting(w http.ResponseWriter, r *http.Request) {
settingName := r.PostFormValue("setting")
settingValue := r.PostFormValue(settingName)
var setting database.Setting
res := s.DbMgr.Db.First(&setting, "name = ?", settingName)
if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) {
set := database.NewSetting(settingName, settingValue)
s.DbMgr.Db.Save(&set)
} else {
s.DbMgr.Db.Model(&setting).Update("value", settingValue)
}
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
}
func (s *Server) HandleNewQuery(w http.ResponseWriter, r *http.Request) {
sub := r.PostFormValue("subUrl")
s.Mutex.Lock()
s.ImageBuffers = make(map[string]*bytes.Buffer)
s.Mutex.Unlock()
s.CurrSubUrl = url
s.PrevSubUrl = ""
s.NextSubUrl = ""
s.LoadCurr()
url := fmt.Sprintf("/title/%s", sub)
go s.LoadNext()
go s.LoadPrev()
session, err := s.getSessionFromCookie(w, r)
if err != nil {
return
}
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)
}

View File

@@ -4,79 +4,135 @@ import (
"bytes"
_ "embed"
"fmt"
"github.com/google/uuid"
"io"
"mangaGetter/internal/database"
"mangaGetter/internal/view"
"net/http"
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/pablu23/mangaGetter/internal/database"
"github.com/pablu23/mangaGetter/internal/provider"
"github.com/pablu23/mangaGetter/internal/view"
)
type Server struct {
ContextManga *database.Manga
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]*bytes.Buffer
Mutex *sync.Mutex
NextSubUrl string
CurrSubUrl string
PrevSubUrl string
IsFirst bool
IsLast bool
DbMgr *database.Manager
}
func New(db *database.Manager) *Server {
func New(provider provider.Provider, db *database.Manager) *Server {
s := Server{
ImageBuffers: make(map[string]*bytes.Buffer),
ImageBuffers: make(map[string][]byte),
Sessions: make(map[string]*UserSession),
Provider: provider,
DbMgr: db,
Mutex: &sync.Mutex{},
Mutex: &sync.RWMutex{},
}
return &s
}
func (s *Server) LoadNext() {
c, err := s.ContextManga.Provider.GetHtml(s.CurrSubUrl)
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) LoadNext(session *UserSession) {
next, err := s.Provider.GetHtml(session.CurrSubUrl)
if err != nil {
fmt.Println(err)
s.NextSubUrl = ""
s.NextViewModel = nil
session.NextSubUrl = ""
session.NextViewModel = nil
return
}
next, err := s.ContextManga.Provider.GetNext(c)
html, err := s.Provider.GetHtml(next)
if err != nil {
fmt.Println(err)
s.NextSubUrl = ""
s.NextViewModel = nil
return
}
html, err := s.ContextManga.Provider.GetHtml(next)
if err != nil {
fmt.Println(err)
s.NextSubUrl = ""
s.NextViewModel = nil
session.NextSubUrl = ""
session.NextViewModel = nil
return
}
imagesNext, err := s.AppendImagesToBuf(html)
if err != nil {
fmt.Println(err)
s.NextSubUrl = ""
s.NextViewModel = nil
session.NextSubUrl = ""
session.NextViewModel = nil
return
}
title, chapter, err := s.ContextManga.Provider.GetTitleAndChapter(next)
title, chapter, err := s.Provider.GetTitleAndChapter(next)
if err != nil {
title = "Unknown"
chapter = "ch_?"
@@ -84,43 +140,43 @@ func (s *Server) LoadNext() {
full := strings.Replace(title, "-", " ", -1) + " - " + strings.Replace(chapter, "_", " ", -1)
s.NextViewModel = &view.ImageViewModel{Images: imagesNext, Title: full}
s.NextSubUrl = next
session.NextViewModel = &view.ImageViewModel{Images: imagesNext, Title: full}
session.NextSubUrl = next
fmt.Println("Loaded next")
}
func (s *Server) LoadPrev() {
c, err := s.ContextManga.Provider.GetHtml(s.CurrSubUrl)
func (s *Server) LoadPrev(session *UserSession) {
c, err := s.Provider.GetHtml(session.CurrSubUrl)
if err != nil {
fmt.Println(err)
s.PrevSubUrl = ""
s.PrevViewModel = nil
session.PrevSubUrl = ""
session.PrevViewModel = nil
return
}
prev, err := s.ContextManga.Provider.GetPrev(c)
prev, err := s.Provider.GetPrev(c)
if err != nil {
fmt.Println(err)
s.PrevSubUrl = ""
s.PrevViewModel = nil
session.PrevSubUrl = ""
session.PrevViewModel = nil
return
}
html, err := s.ContextManga.Provider.GetHtml(prev)
html, err := s.Provider.GetHtml(prev)
if err != nil {
fmt.Println(err)
s.PrevSubUrl = ""
s.PrevViewModel = nil
session.PrevSubUrl = ""
session.PrevViewModel = nil
return
}
imagesNext, err := s.AppendImagesToBuf(html)
if err != nil {
fmt.Println(err)
s.PrevSubUrl = ""
s.PrevViewModel = nil
session.PrevSubUrl = ""
session.PrevViewModel = nil
return
}
title, chapter, err := s.ContextManga.Provider.GetTitleAndChapter(prev)
title, chapter, err := s.Provider.GetTitleAndChapter(prev)
if err != nil {
title = "Unknown"
chapter = "ch_?"
@@ -128,21 +184,21 @@ func (s *Server) LoadPrev() {
full := strings.Replace(title, "-", " ", -1) + " - " + strings.Replace(chapter, "_", " ", -1)
s.PrevViewModel = &view.ImageViewModel{Images: imagesNext, Title: full}
session.PrevViewModel = &view.ImageViewModel{Images: imagesNext, Title: full}
s.PrevSubUrl = prev
session.PrevSubUrl = prev
fmt.Println("Loaded prev")
}
func (s *Server) LoadCurr() {
html, err := s.ContextManga.Provider.GetHtml(s.CurrSubUrl)
func (s *Server) LoadCurr(session *UserSession) {
html, err := s.Provider.GetHtml(session.CurrSubUrl)
if err != nil {
panic(err)
}
imagesCurr, err := s.AppendImagesToBuf(html)
title, chapter, err := s.ContextManga.Provider.GetTitleAndChapter(s.CurrSubUrl)
title, chapter, err := s.Provider.GetTitleAndChapter(session.CurrSubUrl)
if err != nil {
title = "Unknown"
chapter = "ch_?"
@@ -150,33 +206,63 @@ func (s *Server) LoadCurr() {
full := strings.Replace(title, "-", " ", -1) + " - " + strings.Replace(chapter, "_", " ", -1)
s.CurrViewModel = &view.ImageViewModel{Images: imagesCurr, Title: full}
session.CurrViewModel = &view.ImageViewModel{Images: imagesCurr, Title: full}
fmt.Println("Loaded current")
}
func (s *Server) LoadThumbnail(mangaId int) (path string, err error) {
strId := strconv.Itoa(mangaId)
func (s *Server) UpdateLatestAvailableChapter(manga *database.Manga) (error, bool) {
fmt.Printf("Updating Manga: %s\n", manga.Definition.Title)
l, err := s.Provider.GetChapterList("/title/" + strconv.Itoa(manga.Definition.Id))
if err != nil {
return err, false
}
le := len(l)
_, c, err := s.Provider.GetTitleAndChapter(l[le-1])
if err != nil {
return err, false
}
chapterNumberStr := strings.Replace(c, "ch_", "", 1)
if manga.Definition.LastChapterNum == chapterNumberStr {
return nil, false
} else {
manga.Definition.LastChapterNum = chapterNumberStr
return nil, true
}
}
func (s *Server) LoadThumbnail(manga *database.Manga) (path string, updated bool, err error) {
strId := strconv.Itoa(manga.Definition.Id)
s.Mutex.Lock()
defer s.Mutex.Unlock()
if s.ImageBuffers[strId] != nil {
return strId, nil
return strId, false, nil
}
url, err := s.ContextManga.Provider.GetThumbnail(strconv.Itoa(mangaId))
if manga.Definition.Thumbnail != nil {
s.ImageBuffers[strId] = manga.Definition.Thumbnail
return strId, false, nil
}
url, err := s.Provider.GetThumbnail(strId)
if err != nil {
return "", err
return "", false, err
}
ram, err := addFileToRam(url)
if err != nil {
return "", err
return "", false, err
}
manga.Definition.Thumbnail = ram
s.ImageBuffers[strId] = ram
return strId, nil
return strId, true, nil
}
func (s *Server) AppendImagesToBuf(html string) ([]view.Image, error) {
imgList, err := s.ContextManga.Provider.GetImageList(html)
imgList, err := s.Provider.GetImageList(html)
if err != nil {
return nil, err
}
@@ -191,11 +277,11 @@ func (s *Server) AppendImagesToBuf(html string) ([]view.Image, error) {
if err != nil {
panic(err)
}
g := uuid.New()
name := filepath.Base(url)
s.Mutex.Lock()
s.ImageBuffers[g.String()] = buf
s.ImageBuffers[name] = buf
s.Mutex.Unlock()
images[i] = view.Image{Path: g.String(), Index: i}
images[i] = view.Image{Path: name, Index: i}
wg.Done()
}(i, url, &wg)
}
@@ -204,7 +290,7 @@ func (s *Server) AppendImagesToBuf(html string) ([]view.Image, error) {
return images, nil
}
func addFileToRam(url string) (*bytes.Buffer, error) {
func addFileToRam(url string) ([]byte, error) {
// Get the data
resp, err := http.Get(url)
if err != nil {
@@ -221,5 +307,5 @@ func addFileToRam(url string) (*bytes.Buffer, error) {
// Write the body to file
_, err = io.Copy(buf, resp.Body)
return buf, err
return buf.Bytes(), err
}

View File

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

@@ -1,4 +1,5 @@
<!DOCTYPE html>
<!--suppress CssUnusedSymbol -->
<html lang="en">
<head>
<meta charset="UTF-8">
@@ -12,11 +13,16 @@
font-size: 25px;
}
.dark-mode {
.dark {
background-color: #171717;
color: white;
}
.white {
background-color: white;
color: black;
}
.button-36 {
background-image: linear-gradient(92.88deg, #455EB5 9.16%, #5643CC 43.89%, #673FD7 64.72%);
border-radius: 8px;
@@ -101,17 +107,22 @@
text-align: center;
}
label {
display: flex;
align-content: center;
justify-content: center;
margin: 0 auto;
}
select {
width: 10em;
margin-bottom: 10px;
margin-top: 10px;
}
</style>
<script>
function myFunction() {
var element = document.body;
element.classList.toggle("dark-mode");
}
</script>
</head>
<body>
<body class="{{(index .Settings "theme").Value}}">
<form method="post" action="/new/">
<label>
New Sub Url
@@ -119,14 +130,22 @@
</label>
<input type="submit" value="Open" class="button-36">
</form>
<button onclick="myFunction()">Toggle dark mode</button>
<form method="post" action="/setting/">
<label for="theme">Theme</label>
<select onchange="this.form.submit()" id="theme" name="theme">
<option {{if eq (index .Settings "theme").Value "white"}} selected {{end}} value="white">White</option>
<option {{if eq (index .Settings "theme").Value "dark"}} selected {{end}} value="dark">Dark</option>
</select>
<input type="hidden" name="setting" value="theme">
</form>
<table class="table">
<tr>
<th>Thumbnail</th>
<th class="table-left">Title</th>
<th>Current Chapter</th>
<th>Last Accessed</th>
<th class="table-left"><a href="setting/set/order/title">Title</a></th>
<th><a href="setting/set/order/chapter">Current Chapter</a></th>
<th><a href="setting/set/order/last">Last Accessed</a></th>
<th>Link</th>
<th>Delete</th>
</tr>
@@ -138,10 +157,10 @@
</a>
</td>
<td class="table-left">{{.Title}}</td>
<td>{{.Number}}</td>
<td>{{.Number}} / {{.LastNumber}}</td>
<td>{{.LastTime}}</td>
<td>
<a href="/new/{{.Url}}}">
<a href="/new/{{.Url}}">
<button class="button-36">
To chapter
</button>

View File

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

View File

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

74
main.go Normal file
View File

@@ -0,0 +1,74 @@
package main
import (
"fmt"
"github.com/pablu23/mangaGetter/internal/database"
"github.com/pablu23/mangaGetter/internal/provider"
"github.com/pablu23/mangaGetter/internal/server"
"os"
"os/exec"
"os/signal"
"runtime"
"time"
)
func main() {
filePath := getDbPath()
db := database.NewDatabase(filePath, true)
err := db.Open()
if err != nil {
fmt.Println(err)
return
}
s := server.New(&provider.Bato{}, &db)
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
for range c {
Close(&db)
}
}()
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 open(url string) error {
var cmd string
var args []string
switch runtime.GOOS {
case "windows":
cmd = "cmd"
args = []string{"/c", "start"}
case "darwin":
cmd = "open"
default: // "linux", "freebsd", "openbsd", "netbsd"
cmd = "xdg-open"
}
args = append(args, url)
return exec.Command(cmd, args...).Start()
}
func Close(db *database.Manager) {
fmt.Println("Attempting to save and close DB")
err := db.Close()
if err != nil {
fmt.Println(err)
}
os.Exit(0)
}

BIN
mangaGetter Executable file

Binary file not shown.

40
release.go Normal file
View File

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