Compare commits
2 Commits
master
...
Multiple-U
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1fa18fead | ||
|
|
20ad56b155 |
@@ -1,21 +1,24 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
type Chapter struct {
|
type Chapter struct {
|
||||||
Id int `gorm:"primary_key;AUTO_INCREMENT"`
|
Id int `gorm:"primary_key;autoIncrement;"`
|
||||||
|
ChapterId int
|
||||||
Url string
|
Url string
|
||||||
Name string
|
Name string
|
||||||
Number string
|
Number string
|
||||||
TimeStampUnix int64
|
TimeStampUnix int64
|
||||||
MangaId int
|
MangaId int
|
||||||
|
UserId int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewChapter(id int, mangaId int, url string, name string, number string, timeStampUnix int64) Chapter {
|
func NewChapter(id int, mangaId int, userId int, url string, name string, number string, timeStampUnix int64) Chapter {
|
||||||
return Chapter{
|
return Chapter{
|
||||||
Id: id,
|
ChapterId: id,
|
||||||
Url: url,
|
Url: url,
|
||||||
Name: name,
|
Name: name,
|
||||||
Number: number,
|
Number: number,
|
||||||
TimeStampUnix: timeStampUnix,
|
TimeStampUnix: timeStampUnix,
|
||||||
MangaId: mangaId,
|
MangaId: mangaId,
|
||||||
|
UserId: userId,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,6 @@ func (dbMgr *Manager) Delete(mangaId int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (dbMgr *Manager) createDatabaseIfNotExists() error {
|
func (dbMgr *Manager) createDatabaseIfNotExists() error {
|
||||||
err := dbMgr.Db.AutoMigrate(&Manga{}, &Chapter{}, &Setting{})
|
err := dbMgr.Db.AutoMigrate(&MangaDefinition{}, &User{}, &Manga{}, &Chapter{}, &Setting{})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,58 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
type Manga struct {
|
type MangaDefinition struct {
|
||||||
Id int `gorm:"primary_key;AUTO_INCREMENT"`
|
Id int `gorm:"primary_key;AUTO_INCREMENT"`
|
||||||
Title string
|
Title string
|
||||||
TimeStampUnix int64
|
|
||||||
Thumbnail []byte
|
Thumbnail []byte
|
||||||
LastChapterNum string
|
LastChapterNum string
|
||||||
Chapters []Chapter
|
// Chapters []Chapter
|
||||||
//`gorm:"foreignkey:MangaID"`
|
//`gorm:"foreignkey:MangaID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewManga(id int, title string, timeStampUnix int64) Manga {
|
type Manga struct {
|
||||||
return Manga{
|
Id int `gorm:"primary_key;AUTO_INCREMENT"`
|
||||||
|
MangaDefinitionId int
|
||||||
|
Definition MangaDefinition `gorm:"foreignKey:MangaDefinitionId"`
|
||||||
|
UserId int
|
||||||
|
User User
|
||||||
|
TimeStampUnix int64
|
||||||
|
Chapters []Chapter `gorm:"foreignKey:MangaId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMangaDefinition(id int, title string) MangaDefinition {
|
||||||
|
return MangaDefinition{
|
||||||
Id: id,
|
Id: id,
|
||||||
Title: title,
|
Title: title,
|
||||||
TimeStampUnix: timeStampUnix,
|
|
||||||
LastChapterNum: "",
|
LastChapterNum: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewManga(def MangaDefinition, user User, timeStampUnix int64) Manga {
|
||||||
|
return Manga{
|
||||||
|
MangaDefinitionId: def.Id,
|
||||||
|
Definition: def,
|
||||||
|
UserId: user.Id,
|
||||||
|
User: user,
|
||||||
|
TimeStampUnix: timeStampUnix,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GetLatestChapter TODO: Cache this somehow
|
// GetLatestChapter TODO: Cache this somehow
|
||||||
func (m *Manga) GetLatestChapter() (*Chapter, bool) {
|
func (m *Manga) GetLatestChapter() (*Chapter, bool) {
|
||||||
highest := int64(0)
|
// highest := int64(0)
|
||||||
index := 0
|
// index := 0
|
||||||
for i, chapter := range m.Chapters {
|
// for i, chapter := range m.Chapters {
|
||||||
if chapter.MangaId == m.Id && highest < chapter.TimeStampUnix {
|
// if chapter.MangaId == m.Manga.Id && highest < chapter.TimeStampUnix {
|
||||||
highest = chapter.TimeStampUnix
|
// highest = chapter.TimeStampUnix
|
||||||
index = i
|
// index = i
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
if highest == 0 {
|
// if highest == 0 {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
// }
|
||||||
|
|
||||||
return &m.Chapters[index], true
|
// return &m.Chapters[index], true
|
||||||
|
|
||||||
//result := db.Where("manga.id = ?", m.Id).Order("TimeStampUnix desc").Take(&chapter)
|
//result := db.Where("manga.id = ?", m.Id).Order("TimeStampUnix desc").Take(&chapter)
|
||||||
//if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
//if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
|
|||||||
17
internal/database/user.go
Normal file
17
internal/database/user.go
Normal 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"`
|
||||||
|
// }
|
||||||
@@ -5,40 +5,112 @@ import (
|
|||||||
_ "embed"
|
_ "embed"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"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"
|
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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) 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) {
|
func (s *Server) HandleNew(w http.ResponseWriter, r *http.Request) {
|
||||||
title := r.PathValue("title")
|
title := r.PathValue("title")
|
||||||
chapter := r.PathValue("chapter")
|
chapter := r.PathValue("chapter")
|
||||||
|
|
||||||
url := fmt.Sprintf("/title/%s/%s", title, chapter)
|
url := fmt.Sprintf("/title/%s/%s", title, chapter)
|
||||||
|
|
||||||
s.CurrSubUrl = url
|
session, err := s.getSessionFromCookie(w, r)
|
||||||
s.PrevSubUrl = ""
|
if err != nil {
|
||||||
s.NextSubUrl = ""
|
return
|
||||||
s.LoadCurr()
|
}
|
||||||
|
|
||||||
go s.LoadNext()
|
session.CurrSubUrl = url
|
||||||
go s.LoadPrev()
|
session.PrevSubUrl = ""
|
||||||
|
session.NextSubUrl = ""
|
||||||
|
s.LoadCurr(session)
|
||||||
|
|
||||||
|
go s.LoadNext(session)
|
||||||
|
go s.LoadPrev(session)
|
||||||
|
|
||||||
http.Redirect(w, r, "/current/", http.StatusTemporaryRedirect)
|
http.Redirect(w, r, "/current/", http.StatusTemporaryRedirect)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) HandleMenu(w http.ResponseWriter, _ *http.Request) {
|
func (s *Server) HandleMenu(w http.ResponseWriter, r *http.Request) {
|
||||||
tmpl := template.Must(view.GetViewTemplate(view.Menu))
|
tmpl := template.Must(view.GetViewTemplate(view.Menu))
|
||||||
|
|
||||||
|
session, err := s.getSessionFromCookie(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
var all []*database.Manga
|
var all []*database.Manga
|
||||||
_ = s.DbMgr.Db.Preload("Chapters").Find(&all)
|
_ = s.DbMgr.Db.Preload("Chapters").Where("user_id = ?", session.User.Id).Find(&all)
|
||||||
l := len(all)
|
l := len(all)
|
||||||
mangaViewModels := make([]view.MangaViewModel, l)
|
mangaViewModels := make([]view.MangaViewModel, l)
|
||||||
counter := 0
|
counter := 0
|
||||||
@@ -57,7 +129,7 @@ func (s *Server) HandleMenu(w http.ResponseWriter, _ *http.Request) {
|
|||||||
|
|
||||||
//TODO: Change all this to be more performant
|
//TODO: Change all this to be more performant
|
||||||
for _, manga := range all {
|
for _, manga := range all {
|
||||||
title := cases.Title(language.English, cases.Compact).String(strings.Replace(manga.Title, "-", " ", -1))
|
title := cases.Title(language.English, cases.Compact).String(strings.Replace(manga.Definition.Title, "-", " ", -1))
|
||||||
|
|
||||||
t1 := time.Now().UnixNano()
|
t1 := time.Now().UnixNano()
|
||||||
|
|
||||||
@@ -78,30 +150,29 @@ func (s *Server) HandleMenu(w http.ResponseWriter, _ *http.Request) {
|
|||||||
|
|
||||||
// This is very slow
|
// This is very slow
|
||||||
// TODO: put this into own Method
|
// TODO: put this into own Method
|
||||||
if manga.LastChapterNum == "" {
|
if manga.Definition.LastChapterNum == "" {
|
||||||
err, updated := s.UpdateLatestAvailableChapter(manga)
|
err, updated := s.UpdateLatestAvailableChapter(manga)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
if updated {
|
if updated {
|
||||||
s.DbMgr.Db.Save(manga)
|
s.DbMgr.Db.Save(manga.Definition)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
t2 = time.Now().UnixNano()
|
t2 = time.Now().UnixNano()
|
||||||
|
|
||||||
titNs += t2 - t1
|
titNs += t2 - t1
|
||||||
|
|
||||||
latestChapter, ok := manga.GetLatestChapter()
|
latestChapter, ok := manga.GetLatestChapter()
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
mangaViewModels[counter] = view.MangaViewModel{
|
mangaViewModels[counter] = view.MangaViewModel{
|
||||||
ID: manga.Id,
|
ID: manga.Definition.Id,
|
||||||
Title: title,
|
Title: title,
|
||||||
Number: latestChapter.Number,
|
Number: latestChapter.Number,
|
||||||
LastNumber: manga.LastChapterNum,
|
LastNumber: manga.Definition.LastChapterNum,
|
||||||
// I Hate this time Format... 15 = hh, 04 = mm, 02 = DD, 01 = MM, 06 == YY
|
// I Hate this time Format... 15 = hh, 04 = mm, 02 = DD, 01 = MM, 06 == YY
|
||||||
LastTime: time.Unix(manga.TimeStampUnix, 0).Format("15:04 (02-01-06)"),
|
LastTime: time.Unix(manga.TimeStampUnix, 0).Format("15:04 (02-01-06)"),
|
||||||
Url: latestChapter.Url,
|
Url: latestChapter.Url,
|
||||||
@@ -149,7 +220,7 @@ func (s *Server) HandleMenu(w http.ResponseWriter, _ *http.Request) {
|
|||||||
Mangas: mangaViewModels,
|
Mangas: mangaViewModels,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := tmpl.Execute(w, menuViewModel)
|
err = tmpl.Execute(w, menuViewModel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
@@ -178,63 +249,77 @@ func (s *Server) HandleDelete(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (s *Server) HandleExit(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) HandleExit(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||||
|
|
||||||
|
// session, err := s.getSessionFromCookie(w, r)
|
||||||
|
// if err != nil {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
s.Mutex.Lock()
|
// session.Mutex.Lock()
|
||||||
if s.PrevViewModel != nil {
|
// if session.PrevViewModel != nil {
|
||||||
for _, img := range s.PrevViewModel.Images {
|
// for _, img := range session.PrevViewModel.Images {
|
||||||
delete(s.ImageBuffers, img.Path)
|
// delete(s.ImageBuffers, img.Path)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
if s.CurrViewModel != nil {
|
// if session.CurrViewModel != nil {
|
||||||
|
//
|
||||||
for _, img := range s.CurrViewModel.Images {
|
// for _, img := range session.CurrViewModel.Images {
|
||||||
delete(s.ImageBuffers, img.Path)
|
// delete(s.ImageBuffers, img.Path)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
if s.NextViewModel != nil {
|
// if session.NextViewModel != nil {
|
||||||
|
//
|
||||||
for _, img := range s.NextViewModel.Images {
|
// for _, img := range session.NextViewModel.Images {
|
||||||
delete(s.ImageBuffers, img.Path)
|
// delete(s.ImageBuffers, img.Path)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
s.Mutex.Unlock()
|
// session.Mutex.Unlock()
|
||||||
fmt.Println("Cleaned last Manga")
|
fmt.Println("Cleaned last Manga")
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) HandleCurrent(w http.ResponseWriter, _ *http.Request) {
|
func (s *Server) HandleCurrent(w http.ResponseWriter, r *http.Request) {
|
||||||
tmpl := template.Must(view.GetViewTemplate(view.Viewer))
|
tmpl := template.Must(view.GetViewTemplate(view.Viewer))
|
||||||
mangaId, chapterId, err := s.Provider.GetTitleIdAndChapterId(s.CurrSubUrl)
|
session, err := s.getSessionFromCookie(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mangaId, chapterId, err := s.Provider.GetTitleIdAndChapterId(session.CurrSubUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
title, chapterName, err := s.Provider.GetTitleAndChapter(s.CurrSubUrl)
|
title, chapterName, err := s.Provider.GetTitleAndChapter(session.CurrSubUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
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
|
var manga database.Manga
|
||||||
result := s.DbMgr.Db.First(&manga, mangaId)
|
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) {
|
if result.Error != nil && errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
manga = database.NewManga(mangaId, title, time.Now().Unix())
|
manga = database.NewManga(mangaDef, session.User, time.Now().Unix())
|
||||||
} else {
|
|
||||||
manga.TimeStampUnix = time.Now().Unix()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var chapter database.Chapter
|
var chapter database.Chapter
|
||||||
result = s.DbMgr.Db.First(&chapter, chapterId)
|
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) {
|
if result.Error != nil && errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
chapterNumberStr := strings.Replace(chapterName, "ch_", "", 1)
|
chapterNumberStr := strings.Replace(chapterName, "ch_", "", 1)
|
||||||
chapter = database.NewChapter(chapterId, mangaId, s.CurrSubUrl, chapterName, chapterNumberStr, time.Now().Unix())
|
chapter = database.NewChapter(chapterId, mangaId, session.User.Id, session.CurrSubUrl, chapterName, chapterNumberStr, time.Now().Unix())
|
||||||
} else {
|
} else {
|
||||||
chapter.TimeStampUnix = time.Now().Unix()
|
chapter.TimeStampUnix = time.Now().Unix()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.DbMgr.Db.Save(&mangaDef)
|
||||||
s.DbMgr.Db.Save(&manga)
|
s.DbMgr.Db.Save(&manga)
|
||||||
s.DbMgr.Db.Save(&chapter)
|
s.DbMgr.Db.Save(&chapter)
|
||||||
|
|
||||||
err = tmpl.Execute(w, s.CurrViewModel)
|
err = tmpl.Execute(w, session.CurrViewModel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
@@ -242,8 +327,8 @@ func (s *Server) HandleCurrent(w http.ResponseWriter, _ *http.Request) {
|
|||||||
|
|
||||||
func (s *Server) HandleImage(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) HandleImage(w http.ResponseWriter, r *http.Request) {
|
||||||
u := r.PathValue("url")
|
u := r.PathValue("url")
|
||||||
s.Mutex.Lock()
|
s.Mutex.RLock()
|
||||||
defer s.Mutex.Unlock()
|
defer s.Mutex.RUnlock()
|
||||||
buf := s.ImageBuffers[u]
|
buf := s.ImageBuffers[u]
|
||||||
if buf == nil {
|
if buf == nil {
|
||||||
fmt.Printf("url: %s is nil\n", u)
|
fmt.Printf("url: %s is nil\n", u)
|
||||||
@@ -272,7 +357,12 @@ func (s *Server) HandleFavicon(w http.ResponseWriter, _ *http.Request) {
|
|||||||
func (s *Server) HandleNext(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) HandleNext(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Println("Received Next")
|
fmt.Println("Received Next")
|
||||||
|
|
||||||
if s.PrevViewModel != nil {
|
session, err := s.getSessionFromCookie(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if session.PrevViewModel != nil {
|
||||||
go func(viewModel view.ImageViewModel, s *Server) {
|
go func(viewModel view.ImageViewModel, s *Server) {
|
||||||
s.Mutex.Lock()
|
s.Mutex.Lock()
|
||||||
for _, img := range viewModel.Images {
|
for _, img := range viewModel.Images {
|
||||||
@@ -280,26 +370,31 @@ func (s *Server) HandleNext(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
s.Mutex.Unlock()
|
s.Mutex.Unlock()
|
||||||
fmt.Println("Cleaned out of scope Last")
|
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)
|
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.PrevViewModel = s.CurrViewModel
|
session.PrevViewModel = session.CurrViewModel
|
||||||
s.CurrViewModel = s.NextViewModel
|
session.CurrViewModel = session.NextViewModel
|
||||||
s.PrevSubUrl = s.CurrSubUrl
|
session.PrevSubUrl = session.CurrSubUrl
|
||||||
s.CurrSubUrl = s.NextSubUrl
|
session.CurrSubUrl = session.NextSubUrl
|
||||||
|
|
||||||
go s.LoadNext()
|
go s.LoadNext(session)
|
||||||
|
|
||||||
http.Redirect(w, r, "/current/", http.StatusTemporaryRedirect)
|
http.Redirect(w, r, "/current/", http.StatusTemporaryRedirect)
|
||||||
}
|
}
|
||||||
func (s *Server) HandlePrev(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) HandlePrev(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Println("Received Prev")
|
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) {
|
go func(viewModel view.ImageViewModel, s *Server) {
|
||||||
s.Mutex.Lock()
|
s.Mutex.Lock()
|
||||||
for _, img := range viewModel.Images {
|
for _, img := range viewModel.Images {
|
||||||
@@ -307,20 +402,20 @@ func (s *Server) HandlePrev(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
s.Mutex.Unlock()
|
s.Mutex.Unlock()
|
||||||
fmt.Println("Cleaned out of scope Last")
|
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)
|
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.NextViewModel = s.CurrViewModel
|
session.NextViewModel = session.CurrViewModel
|
||||||
s.CurrViewModel = s.PrevViewModel
|
session.CurrViewModel = session.PrevViewModel
|
||||||
s.NextSubUrl = s.CurrSubUrl
|
session.NextSubUrl = session.CurrSubUrl
|
||||||
s.CurrSubUrl = s.PrevSubUrl
|
session.CurrSubUrl = session.PrevSubUrl
|
||||||
|
|
||||||
go s.LoadPrev()
|
go s.LoadPrev(session)
|
||||||
|
|
||||||
http.Redirect(w, r, "/current/", http.StatusTemporaryRedirect)
|
http.Redirect(w, r, "/current/", http.StatusTemporaryRedirect)
|
||||||
}
|
}
|
||||||
@@ -364,13 +459,17 @@ func (s *Server) HandleNewQuery(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
url := fmt.Sprintf("/title/%s", sub)
|
url := fmt.Sprintf("/title/%s", sub)
|
||||||
|
|
||||||
s.CurrSubUrl = url
|
session, err := s.getSessionFromCookie(w, r)
|
||||||
s.PrevSubUrl = ""
|
if err != nil {
|
||||||
s.NextSubUrl = ""
|
return
|
||||||
s.LoadCurr()
|
}
|
||||||
|
session.CurrSubUrl = url
|
||||||
|
session.PrevSubUrl = ""
|
||||||
|
session.NextSubUrl = ""
|
||||||
|
s.LoadCurr(session)
|
||||||
|
|
||||||
go s.LoadNext()
|
go s.LoadNext(session)
|
||||||
go s.LoadPrev()
|
go s.LoadPrev(session)
|
||||||
|
|
||||||
http.Redirect(w, r, "/current/", http.StatusTemporaryRedirect)
|
http.Redirect(w, r, "/current/", http.StatusTemporaryRedirect)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,50 +4,57 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/pablu23/mangaGetter/internal/database"
|
|
||||||
"github.com/pablu23/mangaGetter/internal/provider"
|
|
||||||
"github.com/pablu23/mangaGetter/internal/view"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
"github.com/pablu23/mangaGetter/internal/database"
|
||||||
|
"github.com/pablu23/mangaGetter/internal/provider"
|
||||||
|
"github.com/pablu23/mangaGetter/internal/view"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
|
ImageBuffers map[string][]byte
|
||||||
|
Provider provider.Provider
|
||||||
|
DbMgr *database.Manager
|
||||||
|
|
||||||
|
Mutex *sync.RWMutex
|
||||||
|
|
||||||
|
Sessions map[string]*UserSession
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserSession struct {
|
||||||
|
User database.User
|
||||||
|
|
||||||
|
// Mutex *sync.Mutex
|
||||||
|
|
||||||
|
PrevSubUrl string
|
||||||
|
CurrSubUrl string
|
||||||
|
NextSubUrl string
|
||||||
|
|
||||||
PrevViewModel *view.ImageViewModel
|
PrevViewModel *view.ImageViewModel
|
||||||
CurrViewModel *view.ImageViewModel
|
CurrViewModel *view.ImageViewModel
|
||||||
NextViewModel *view.ImageViewModel
|
NextViewModel *view.ImageViewModel
|
||||||
|
|
||||||
ImageBuffers map[string][]byte
|
|
||||||
Mutex *sync.Mutex
|
|
||||||
|
|
||||||
NextSubUrl string
|
|
||||||
CurrSubUrl string
|
|
||||||
PrevSubUrl string
|
|
||||||
|
|
||||||
Provider provider.Provider
|
|
||||||
|
|
||||||
IsFirst bool
|
|
||||||
IsLast bool
|
|
||||||
|
|
||||||
DbMgr *database.Manager
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(provider provider.Provider, db *database.Manager) *Server {
|
func New(provider provider.Provider, db *database.Manager) *Server {
|
||||||
s := Server{
|
s := Server{
|
||||||
ImageBuffers: make(map[string][]byte),
|
ImageBuffers: make(map[string][]byte),
|
||||||
|
Sessions: make(map[string]*UserSession),
|
||||||
Provider: provider,
|
Provider: provider,
|
||||||
DbMgr: db,
|
DbMgr: db,
|
||||||
Mutex: &sync.Mutex{},
|
Mutex: &sync.RWMutex{},
|
||||||
}
|
}
|
||||||
|
|
||||||
return &s
|
return &s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Start(port int) error {
|
func (s *Server) Start(port int) error {
|
||||||
|
http.HandleFunc("/register", s.HandleRegister)
|
||||||
|
http.HandleFunc("/login", s.HandleLogin)
|
||||||
http.HandleFunc("/", s.HandleMenu)
|
http.HandleFunc("/", s.HandleMenu)
|
||||||
http.HandleFunc("/new/", s.HandleNewQuery)
|
http.HandleFunc("/new/", s.HandleNewQuery)
|
||||||
http.HandleFunc("/new/title/{title}/{chapter}", s.HandleNew)
|
http.HandleFunc("/new/title/{title}/{chapter}", s.HandleNew)
|
||||||
@@ -62,74 +69,66 @@ func (s *Server) Start(port int) error {
|
|||||||
http.HandleFunc("GET /setting/set/{setting}/{value}", s.HandleSettingSet)
|
http.HandleFunc("GET /setting/set/{setting}/{value}", s.HandleSettingSet)
|
||||||
|
|
||||||
// Update Latest Chapters every 5 Minutes
|
// Update Latest Chapters every 5 Minutes
|
||||||
go func(s *Server) {
|
// go func(s *Server) {
|
||||||
time.AfterFunc(time.Second*10, func() {
|
// time.AfterFunc(time.Second*10, func() {
|
||||||
var all []*database.Manga
|
// var all []*database.Manga
|
||||||
s.DbMgr.Db.Find(&all)
|
// s.DbMgr.Db.Find(&all)
|
||||||
for _, m := range all {
|
// for _, m := range all {
|
||||||
err, updated := s.UpdateLatestAvailableChapter(m)
|
// err, updated := s.UpdateLatestAvailableChapter(m)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
fmt.Println(err)
|
// fmt.Println(err)
|
||||||
}
|
// }
|
||||||
if updated {
|
// if updated {
|
||||||
s.DbMgr.Db.Save(m)
|
// s.DbMgr.Db.Save(m)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
|
//
|
||||||
for {
|
// for {
|
||||||
select {
|
// select {
|
||||||
case <-time.After(time.Minute * 5):
|
// case <-time.After(time.Minute * 5):
|
||||||
var all []*database.Manga
|
// var all []*database.Manga
|
||||||
s.DbMgr.Db.Find(&all)
|
// s.DbMgr.Db.Find(&all)
|
||||||
for _, m := range all {
|
// for _, m := range all {
|
||||||
err, updated := s.UpdateLatestAvailableChapter(m)
|
// err, updated := s.UpdateLatestAvailableChapter(m)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
fmt.Println(err)
|
// fmt.Println(err)
|
||||||
}
|
// }
|
||||||
if updated {
|
// if updated {
|
||||||
s.DbMgr.Db.Save(m)
|
// s.DbMgr.Db.Save(m)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}(s)
|
// }(s)
|
||||||
|
//
|
||||||
fmt.Println("Server starting...")
|
fmt.Println("Server starting...")
|
||||||
err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
|
err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) LoadNext() {
|
func (s *Server) LoadNext(session *UserSession) {
|
||||||
c, err := s.Provider.GetHtml(s.CurrSubUrl)
|
next, err := s.Provider.GetHtml(session.CurrSubUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
s.NextSubUrl = ""
|
session.NextSubUrl = ""
|
||||||
s.NextViewModel = nil
|
session.NextViewModel = nil
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
next, err := s.Provider.GetNext(c)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
s.NextSubUrl = ""
|
|
||||||
s.NextViewModel = nil
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
html, err := s.Provider.GetHtml(next)
|
html, err := s.Provider.GetHtml(next)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
s.NextSubUrl = ""
|
session.NextSubUrl = ""
|
||||||
s.NextViewModel = nil
|
session.NextViewModel = nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
imagesNext, err := s.AppendImagesToBuf(html)
|
imagesNext, err := s.AppendImagesToBuf(html)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
s.NextSubUrl = ""
|
session.NextSubUrl = ""
|
||||||
s.NextViewModel = nil
|
session.NextViewModel = nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,39 +140,39 @@ func (s *Server) LoadNext() {
|
|||||||
|
|
||||||
full := strings.Replace(title, "-", " ", -1) + " - " + strings.Replace(chapter, "_", " ", -1)
|
full := strings.Replace(title, "-", " ", -1) + " - " + strings.Replace(chapter, "_", " ", -1)
|
||||||
|
|
||||||
s.NextViewModel = &view.ImageViewModel{Images: imagesNext, Title: full}
|
session.NextViewModel = &view.ImageViewModel{Images: imagesNext, Title: full}
|
||||||
s.NextSubUrl = next
|
session.NextSubUrl = next
|
||||||
fmt.Println("Loaded next")
|
fmt.Println("Loaded next")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) LoadPrev() {
|
func (s *Server) LoadPrev(session *UserSession) {
|
||||||
c, err := s.Provider.GetHtml(s.CurrSubUrl)
|
c, err := s.Provider.GetHtml(session.CurrSubUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
s.PrevSubUrl = ""
|
session.PrevSubUrl = ""
|
||||||
s.PrevViewModel = nil
|
session.PrevViewModel = nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
prev, err := s.Provider.GetPrev(c)
|
prev, err := s.Provider.GetPrev(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
s.PrevSubUrl = ""
|
session.PrevSubUrl = ""
|
||||||
s.PrevViewModel = nil
|
session.PrevViewModel = nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
html, err := s.Provider.GetHtml(prev)
|
html, err := s.Provider.GetHtml(prev)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
s.PrevSubUrl = ""
|
session.PrevSubUrl = ""
|
||||||
s.PrevViewModel = nil
|
session.PrevViewModel = nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
imagesNext, err := s.AppendImagesToBuf(html)
|
imagesNext, err := s.AppendImagesToBuf(html)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
s.PrevSubUrl = ""
|
session.PrevSubUrl = ""
|
||||||
s.PrevViewModel = nil
|
session.PrevViewModel = nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,21 +184,21 @@ func (s *Server) LoadPrev() {
|
|||||||
|
|
||||||
full := strings.Replace(title, "-", " ", -1) + " - " + strings.Replace(chapter, "_", " ", -1)
|
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")
|
fmt.Println("Loaded prev")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) LoadCurr() {
|
func (s *Server) LoadCurr(session *UserSession) {
|
||||||
html, err := s.Provider.GetHtml(s.CurrSubUrl)
|
html, err := s.Provider.GetHtml(session.CurrSubUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
imagesCurr, err := s.AppendImagesToBuf(html)
|
imagesCurr, err := s.AppendImagesToBuf(html)
|
||||||
|
|
||||||
title, chapter, err := s.Provider.GetTitleAndChapter(s.CurrSubUrl)
|
title, chapter, err := s.Provider.GetTitleAndChapter(session.CurrSubUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
title = "Unknown"
|
title = "Unknown"
|
||||||
chapter = "ch_?"
|
chapter = "ch_?"
|
||||||
@@ -207,14 +206,14 @@ func (s *Server) LoadCurr() {
|
|||||||
|
|
||||||
full := strings.Replace(title, "-", " ", -1) + " - " + strings.Replace(chapter, "_", " ", -1)
|
full := strings.Replace(title, "-", " ", -1) + " - " + strings.Replace(chapter, "_", " ", -1)
|
||||||
|
|
||||||
s.CurrViewModel = &view.ImageViewModel{Images: imagesCurr, Title: full}
|
session.CurrViewModel = &view.ImageViewModel{Images: imagesCurr, Title: full}
|
||||||
fmt.Println("Loaded current")
|
fmt.Println("Loaded current")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) UpdateLatestAvailableChapter(manga *database.Manga) (error, bool) {
|
func (s *Server) UpdateLatestAvailableChapter(manga *database.Manga) (error, bool) {
|
||||||
fmt.Printf("Updating Manga: %s\n", manga.Title)
|
fmt.Printf("Updating Manga: %s\n", manga.Definition.Title)
|
||||||
|
|
||||||
l, err := s.Provider.GetChapterList("/title/" + strconv.Itoa(manga.Id))
|
l, err := s.Provider.GetChapterList("/title/" + strconv.Itoa(manga.Definition.Id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err, false
|
return err, false
|
||||||
}
|
}
|
||||||
@@ -227,16 +226,16 @@ func (s *Server) UpdateLatestAvailableChapter(manga *database.Manga) (error, boo
|
|||||||
|
|
||||||
chapterNumberStr := strings.Replace(c, "ch_", "", 1)
|
chapterNumberStr := strings.Replace(c, "ch_", "", 1)
|
||||||
|
|
||||||
if manga.LastChapterNum == chapterNumberStr {
|
if manga.Definition.LastChapterNum == chapterNumberStr {
|
||||||
return nil, false
|
return nil, false
|
||||||
} else {
|
} else {
|
||||||
manga.LastChapterNum = chapterNumberStr
|
manga.Definition.LastChapterNum = chapterNumberStr
|
||||||
return nil, true
|
return nil, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) LoadThumbnail(manga *database.Manga) (path string, updated bool, err error) {
|
func (s *Server) LoadThumbnail(manga *database.Manga) (path string, updated bool, err error) {
|
||||||
strId := strconv.Itoa(manga.Id)
|
strId := strconv.Itoa(manga.Definition.Id)
|
||||||
|
|
||||||
s.Mutex.Lock()
|
s.Mutex.Lock()
|
||||||
defer s.Mutex.Unlock()
|
defer s.Mutex.Unlock()
|
||||||
@@ -244,8 +243,8 @@ func (s *Server) LoadThumbnail(manga *database.Manga) (path string, updated bool
|
|||||||
return strId, false, nil
|
return strId, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if manga.Thumbnail != nil {
|
if manga.Definition.Thumbnail != nil {
|
||||||
s.ImageBuffers[strId] = manga.Thumbnail
|
s.ImageBuffers[strId] = manga.Definition.Thumbnail
|
||||||
return strId, false, nil
|
return strId, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,7 +256,7 @@ func (s *Server) LoadThumbnail(manga *database.Manga) (path string, updated bool
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", false, err
|
return "", false, err
|
||||||
}
|
}
|
||||||
manga.Thumbnail = ram
|
manga.Definition.Thumbnail = ram
|
||||||
s.ImageBuffers[strId] = ram
|
s.ImageBuffers[strId] = ram
|
||||||
return strId, true, nil
|
return strId, true, nil
|
||||||
}
|
}
|
||||||
|
|||||||
31
internal/utils/concurrent_map.go
Normal file
31
internal/utils/concurrent_map.go
Normal 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
|
||||||
|
}
|
||||||
BIN
mangaGetter
BIN
mangaGetter
Binary file not shown.
Reference in New Issue
Block a user