Author: Adam <git@apiote.tk>
show watchlist
datastructure/item.go | 49 ++++++++++++++++++++ datastructure/watchlist.go | 31 ++++++++++++ db/db.go | 59 ++++++++++++++++++++++++ front/capnproto.go | 7 ++ front/html.go | 7 ++ front/renderer.go | 2 libamuse/account.go | 7 ++ libamuse/watchlist.go | 65 +++++++++++++++++++++++++++ router.go | 7 ++ static/style/style.css | 8 +++ templates/watchlist.html | 96 ++++++++++++++++++++++++++++++++++++++++ tmdb/film.go | 4 + tmdb/genres.go | 64 ++++++++++++++++++++++++++ tmdb/serie.go | 4 +
diff --git a/datastructure/item.go b/datastructure/item.go new file mode 100644 index 0000000000000000000000000000000000000000..77383d4f284743907f1f204535254f9c2430a99f --- /dev/null +++ b/datastructure/item.go @@ -0,0 +1,49 @@ +package datastructure + +import ( + "strconv" + "strings" +) + +type ItemInfo struct { + Cover string + Status string + Title string + YearStart int + YearEnd int + BasedOn string + Genres string + Runtime int + Collection int + Part int +} + +func (i ItemInfo) IsUnreleased() bool { + return i.Status != "Released" +} + +func (i ItemInfo) GetGenres(genres map[int]string) string { + genreIds := strings.Split(i.Genres, ",") + genreNames := []string{} + for _, genreId := range genreIds { + if genreId != "" { + genreIdInt, _ := strconv.ParseInt(genreId, 10, 64) + genreNames = append(genreNames, genres[int(genreIdInt)]) + } + } + return strings.Join(genreNames, ", ") +} + +type Item interface { + GetItemInfo() ItemInfo + GetItemType() ItemType +} + +type ItemType string + +const ( + ItemTypeBook ItemType = "book" + ItemTypeFilm = "film" + ItemTypeTvserie = "tvserie" + ItemTypeUnkown = "unknown" +) diff --git a/datastructure/watchlist.go b/datastructure/watchlist.go new file mode 100644 index 0000000000000000000000000000000000000000..8fbfeaae6aab4a43c6eb35456f0721a4ee467810 --- /dev/null +++ b/datastructure/watchlist.go @@ -0,0 +1,31 @@ +package datastructure + +type WatchlistEntry struct { + ItemInfo + Id string + HasPrevious bool +} + +type Watchlist struct { + List []WatchlistEntry + Page int + Pages int + Genres map[int]string + Query string +} + +func (w Watchlist) NextPage() int { + if w.Page < w.Pages { + return w.Page + 1 + } else { + return w.Page + } +} + +func (w Watchlist) PrevPage() int { + if w.Page > 1 { + return w.Page - 1 + } else { + return w.Page + } +} diff --git a/db/db.go b/db/db.go index 4063a8233a1f9efeedbb2f05bfa2686d8b4c28b8..f12aedfe95a3ab640bef36e4151ac67d2decc7ae 100644 --- a/db/db.go +++ b/db/db.go @@ -9,6 +9,7 @@ "database/sql" "encoding/hex" "errors" "fmt" + "math" "os" "sort" "time" @@ -515,3 +516,61 @@ _, err = db.Exec(`insert into cache values(?, ?, null, ?, datetime('now')) on conflict(uri) do update set etag = excluded.etag, response = excluded.response, last_hit = excluded.last_hit`, uri, etag, data) return err } + +func GetWatchlist(username, filter string, page int) (datastructure.Watchlist, error) { + watchlist := datastructure.Watchlist{} + db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db") + if err != nil { + fmt.Fprintf(os.Stderr, "DB open err\n") + return watchlist, err + } + defer db.Close() + + if page <= 0 { + page = 1 + } + + var pages float64 + row := db.QueryRow(`select count(*) from wantlist where item_type = 'film' and username = ?`, username) + err = row.Scan(&pages) + if err != nil { + return watchlist, err + } + watchlist.Pages = int(math.Ceil(pages / 18)) + + offset := (page - 1) * 18 + + //todo filter, order by + + var whereClause string + if filter != "" { + whereClause = "and c1.title like '%" + filter + "%'" + } + + rows, err := db.Query(`select c1.item_id, c1.cover, c1.status, c1.title, c1.year_start, c1.based_on, c1.genres, c1.runtime, c1.part, c2.part from wantlist natural join item_cache c1 left join item_cache c2 on(c1.part-1 = c2.part and c1.collection = c2.collection) where c1.item_type = 'film' and username = ? `+whereClause+` order by c1.title limit ?,18`, username, offset) + + if err != nil { + fmt.Fprintf(os.Stderr, "Select err: %v\n", err) + return watchlist, err + } + defer rows.Close() + + for rows.Next() { + var ( + entry datastructure.WatchlistEntry + prevPart *int + ) + err := rows.Scan(&entry.Id, &entry.Cover, &entry.Status, &entry.Title, &entry.YearStart, &entry.BasedOn, &entry.Genres, &entry.Runtime, &entry.Part, &prevPart) + if err != nil { + fmt.Println("Scan error") + return datastructure.Watchlist{}, err + } + + if prevPart != nil { + entry.HasPrevious = true + } + watchlist.List = append(watchlist.List, entry) + } + + return watchlist, nil +} diff --git a/front/capnproto.go b/front/capnproto.go index 14a0184c16b95bad5f7a2bd54357f875e21b60d6..3d87d7b473c763b8789d757fb46a33f5478a411f 100644 --- a/front/capnproto.go +++ b/front/capnproto.go @@ -3,6 +3,7 @@ import ( "notabug.org/apiote/amuse/tmdb" "notabug.org/apiote/amuse/wikidata" + "notabug.org/apiote/amuse/datastructure" "golang.org/x/text/language" ) @@ -38,7 +39,7 @@ return TODO("implement CapnprotoRenderer.RenderBook").(string) } func (CapnprotoRenderer) RenderBookSerie(bookSerie wikidata.BookSerie, languages []language.Tag) string { - return TODO("implement CapnprotoRenderer.RenderBook").(string) + return TODO("implement CapnprotoRenderer.RenderBookSerie").(string) } func (CapnprotoRenderer) RenderAbout(languages []language.Tag) string { @@ -53,3 +54,7 @@ func (CapnprotoRenderer) RenderLogin(languages []language.Tag, err error, target string) string { // todo throw Wrong Accept return TODO("implement CapnprotoRenderer.RenderLogin").(string) } + +func (CapnprotoRenderer) RenderWatchlist(watchlist datastructure.Watchlist, languages []language.Tag) string { + return TODO("implement CapnprotoRenderer.RenderWatchlist").(string) +} diff --git a/front/html.go b/front/html.go index f9babed07bac652d8b2fb49cda79881f3b9c673a..9f3c0d199770332eef67a18153d830b1fc5ca1e2 100644 --- a/front/html.go +++ b/front/html.go @@ -6,6 +6,7 @@ "notabug.org/apiote/amuse/i18n" "notabug.org/apiote/amuse/tmdb" "notabug.org/apiote/amuse/utils" "notabug.org/apiote/amuse/wikidata" + "notabug.org/apiote/amuse/datastructure" "bytes" "golang.org/x/text/language" @@ -132,3 +133,9 @@ data := RenderData{Data: target} data.State.Error = authError return render(languages, data, "login") } + +func (r HtmlRenderer) RenderWatchlist(watchlist datastructure.Watchlist, languages []language.Tag) string { + data := RenderData{Data: watchlist} + data.State.User = r.user + return render(languages, data, "watchlist") +} diff --git a/front/renderer.go b/front/renderer.go index ef4a214b937db471be5a3ba99c558ea1b9b82b2e..9c083c6e7cbe6fb5b35367825d986613462d0cd6 100644 --- a/front/renderer.go +++ b/front/renderer.go @@ -4,6 +4,7 @@ import ( "notabug.org/apiote/amuse/accounts" "notabug.org/apiote/amuse/tmdb" "notabug.org/apiote/amuse/wikidata" + "notabug.org/apiote/amuse/datastructure" "golang.org/x/text/language" ) @@ -27,6 +28,7 @@ RenderBookSerie(wikidata.BookSerie, []language.Tag) string RenderAbout([]language.Tag) string RenderErrorPage(int, []language.Tag) string RenderLogin([]language.Tag, error, string) string + RenderWatchlist(datastructure.Watchlist, []language.Tag) string } func NewRenderer(mimetype string, user accounts.User) (Renderer, error) { diff --git a/libamuse/account.go b/libamuse/account.go index e70673687b53af0ca5b2053b26bd902257e4624d..d52205d5f926fe38638d5f687b6212e972345860 100644 --- a/libamuse/account.go +++ b/libamuse/account.go @@ -16,6 +16,7 @@ ) func VerifyAuthToken(token accounts.Authentication) (accounts.User, error) { if token.Token == "" { + fmt.Fprintf(os.Stderr, "Empty token\n") return accounts.User{}, nil } session, err := db.GetSession(token.Token) @@ -83,9 +84,11 @@ func cacheItem(args ...interface{}) (interface{}, error) { data := args[0].(*RequestData) result := args[1].(*Result) - itemInfo := result.result.(datastructure.Item).GetItemInfo() + item := result.result.(datastructure.Item) - err := db.SaveCacheItem(datastructure.ItemTypeFilm, data.id, itemInfo) + itemInfo := item.GetItemInfo() + + err := db.SaveCacheItem(item.GetItemType(), data.id, itemInfo) return gott.Tuple(args), err } diff --git a/libamuse/watchlist.go b/libamuse/watchlist.go new file mode 100644 index 0000000000000000000000000000000000000000..cd8e9bc9d161e4b80d7958c77a66c0ac0fd9ca52 --- /dev/null +++ b/libamuse/watchlist.go @@ -0,0 +1,65 @@ +package libamuse + +import ( + "notabug.org/apiote/amuse/accounts" + "notabug.org/apiote/amuse/tmdb" + "notabug.org/apiote/amuse/db" + "notabug.org/apiote/amuse/datastructure" + + "notabug.org/apiote/gott" +) + +func getWatchlist(args ...interface{}) (interface{}, error) { + request := args[0].(*RequestData) + result := args[1].(*Result) + page := args[2].(int) + watchlist, err := db.GetWatchlist(result.user.Username, request.id, page) + result.result = watchlist + + return gott.Tuple(args), err +} + +func renderWatchlist(args ...interface{}) interface{} { + request := args[0].(*RequestData) + result := args[1].(*Result) + page := args[2].(int) + watchlist := result.result.(datastructure.Watchlist) + watchlist.Page = page + watchlist.Query = request.id + result.page = result.renderer.RenderWatchlist(watchlist, result.languages) + + return gott.Tuple(args) +} + +func getGenres(args ...interface{}) (interface{}, error) { + result := args[1].(*Result) + watchlist := result.result.(datastructure.Watchlist) + genres, err := tmdb.GetGenres(result.languages[0].String()) + watchlist.Genres = genres + result.result = watchlist + return gott.Tuple(args), err +} + +func ShowWatchlist(username string, auth accounts.Authentication, languages, mimetype, filter string, page int) (string, error) { + auth.Necessary = true + if page <= 0 { + page = 1 + } + request := &RequestData{id: filter, language: languages, mimetype: mimetype, auth: auth, username: username} + r, err := gott. + NewResult(gott.Tuple{request, &Result{}, page}). + Bind(parseLanguage). + Bind(verifyToken). + Bind(verifyUser). + Bind(getWatchlist). + Bind(getGenres). + Bind(createRenderer). + Map(renderWatchlist). + Finish() + + if err != nil { + return "", err + } else { + return r.(gott.Tuple)[1].(*Result).page, nil + } +} diff --git a/router.go b/router.go index 1dafc435cbb5cdd59ba904ae9c0d6d0d4da3f07b..71772443b91926905b3d8951d71ce4e0f1149b94 100644 --- a/router.go +++ b/router.go @@ -278,7 +278,12 @@ } func userWatchlist(w http.ResponseWriter, r *http.Request, username string, auth accounts.Authentication, acceptLanguages string, mimetype string) { if r.Method == "" || r.Method == "GET" { - // todo + var page int + r.ParseForm() + filter := r.Form.Get("filter") + fmt.Sscanf(r.Form.Get("page"), "%d", &page) + watchlist, err := libamuse.ShowWatchlist(username, auth, acceptLanguages, mimetype, filter, page) + render(watchlist, err, w, acceptLanguages, mimetype) } else if r.Method == "POST" { r.ParseForm() itemId := r.PostForm.Get("itemId") diff --git a/static/style/style.css b/static/style/style.css index a282d2a95d9c376c4de68151e2ab30a295b81209..1f5d73f48342cad85ab13e64a6cf3c5495f32f14 100644 --- a/static/style/style.css +++ b/static/style/style.css @@ -527,6 +527,14 @@ .spoler:hover { filter: blur(0) !important; } +.bw { + filter: grayscale(100%); +} + +.bw:hover { + filter: grayscale(0) !important; +} + /* POSITION */ .right { diff --git a/templates/watchlist.html b/templates/watchlist.html new file mode 100644 index 0000000000000000000000000000000000000000..d65dcfae4dc3ffcfe0f6e600c17f9da9bd75089d --- /dev/null +++ b/templates/watchlist.html @@ -0,0 +1,96 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>{{.State.User.Username}}’s watchilst — a·muse</title> + <link rel="stylesheet" href="/static/style/style.css" /> + <link rel="icon" type="image/svg+xml" href="/static/img/logo.svg"> + <link rel="apple-touch-icon" type="image/svg+xml" href="/static/img/logo.svg"> + </head> + <body> + <header class="w12 padding-bottom-_25 flex flex-row flex-justify-space flex-align-centre"> + <a href="/" class="decoration-none"> + <h1 class="inline valign-mid text sans margin-lr-1">a·muse</h1> + </a> + <div class="margin-lr-1 text"> + <nav> + <label for="hamburger" class="cursor-hand"> + <img src="/users/{{.State.User.Username}}/avatar?size=small" class="border-radius-25 width-1_5"/> + </label> + <input type="checkbox" id="hamburger" class="display-none" /> + <ul class="absolute right top-1 padding-lr-1 padding-tb-_5 bg align-right list-style-none sans"> + <li><a href="/users/{{.State.User.Username}}" class="decoration-none text-accent">Profile</a><span class="material-icon padding-lr-_5"></span></li> + <li><a href="/users/{{.State.User.Username}}/watchlist" class="decoration-none text-accent">Watchlist</a><span class="material-icon padding-lr-_5"></span></li> + <li><a href="/users/{{.State.User.Username}}/readlist" class="decoration-none text-accent">Readlist</a><span class="material-icon padding-lr-_5"></span></li> + <li><a href="/users/{{.State.User.Username}}/experiences" class="decoration-none text-accent">Experiences</a><span class="material-icon padding-lr-_5"></span></li> + <li class="bg-error"> + <form action="/users/{{.State.User.Username}}/sessions/{{.State.User.Session}}" method="POST" class="inline"> + <input type="hidden" value="DELETE" name="method" /> + <input type="submit" value="Log out" class="border-none bg-none font-normal text-accent padding-lr-0 cursor-hand font-1" /> + </form><span class="material-icon padding-lr-_5"></span> + </li> + </ul> + </nav> + </div> + </header> + <main class="margin-lr-1"> + <!-- search, filter, order --> + <div class="flex flex-row flex-wrap flex-centre flex-align-start margin-top-1"> + <form method="GET" class="flex inline margin-lr-1 border-bottom"> + <input type="search" name="filter" class="border-none bg-none sans text" placeholder="filter watchlist" value="{{.Data.Query}}" /> + </form> + </div> + <div class="flex flex-row flex-wrap flex-justify-space flex-align-start margin-top-1"> + <div> + {{if gt .Data.Page 1}} + <a href="/users/{{.State.User.Username}}/watchlist?filter={{.Data.Query}}&page={{.Data.PrevPage}}" class="decoration-none" title="{{.Strings.Search.prev_link_title}}"><span class="material-icon font-2"></span></a> + {{end}} + </div> + <div> + {{if lt .Data.Page .Data.Pages}} + <a href="/users/{{.State.User.Username}}/watchlist?filter={{.Data.Query}}&page={{.Data.NextPage}}" class="decoration-none" title="{{.Strings.Search.next_link_title}}"><span class="material-icon font-2"></span></a> + {{end}} + </div> + </div> + <div class="flex flex-row flex-wrap flex-justify-space flex-align-start"> + {{range .Data.List}} + <a href="/films/{{.Id}}" class="decoration-none force-width-18 margin-tb-1 no-outline"> + <div class="flex"> + <div> + {{if .Cover}} + <img src="https://image.tmdb.org/t/p/w154{{.Cover}}" class="width-154px{{if .IsUnreleased}} bw{{end}}" /> + {{else}} + <img src="/static/img/poster_empty.webp" class="width-154px" /> + {{end}} + </div> + <div class="margin-lr-1"> + <p class="sans">{{.Title}}</p> + <p class="sans font-_875 text-grey">{{.YearStart}}</p> + <p class="sans font-_875"> + {{.GetGenres $.Data.Genres}} + </p> + <p class="font-_875 text-grey"> + {{if .HasPrevious}}<span class="material-icon" title="Watch previous part first"></span>{{end}} + <!-- todo based on --> + </p> + </div> + </div> + </a> + {{end}} + </div> + <div class="flex flex-row flex-wrap flex-justify-space flex-align-start margin-top-1"> + <div> + {{if gt .Data.Page 1}} + <a href="/users/{{.State.User.Username}}/watchlist?filter={{.Data.Query}}&page={{.Data.PrevPage}}" class="decoration-none" title="{{.Strings.Search.prev_link_title}}"><span class="material-icon font-2"></span></a> + {{end}} + </div> + <div> + {{if lt .Data.Page .Data.Pages}} + <a href="/users/{{.State.User.Username}}/watchlist?filter={{.Data.Query}}&page={{.Data.NextPage}}" class="decoration-none" title="{{.Strings.Search.next_link_title}}"><span class="material-icon font-2"></span></a> + {{end}} + </div> + </div> + </main> + </body> +</html> diff --git a/tmdb/film.go b/tmdb/film.go index fdf46c326a5faf457371f9887bcd63cdb0798a93..628a4de850d48555ce6e1583c0d7d8f03c675909 100644 --- a/tmdb/film.go +++ b/tmdb/film.go @@ -82,6 +82,10 @@ } return itemInfo } +func (f *Film) GetItemType() datastructure.ItemType { + return datastructure.ItemTypeFilm +} + func (f *Film) AddBasedOn(book wikidata.Book) { f.BasedOn = book } diff --git a/tmdb/genres.go b/tmdb/genres.go new file mode 100644 index 0000000000000000000000000000000000000000..d2c049e5fc9fe2e8b1a4e360bf326cb9231be43e --- /dev/null +++ b/tmdb/genres.go @@ -0,0 +1,64 @@ +package tmdb + +import ( + "notabug.org/apiote/amuse/network" + + "encoding/json" + "net/http" + + "notabug.org/apiote/gott" +) + +type Genres struct { + Genres []struct { + Id int + Name string + } +} + +func createGenresRequest(args ...interface{}) (interface{}, error) { + request := args[0].(*network.Request) + result := args[1].(*network.Result) + result.Client = &http.Client{} + httpRequest, err := http.NewRequest("GET", "https://api.themoviedb.org/3/genre/movie/list?api_key="+API_KEY+"&language="+request.Language, nil) + result.Request = httpRequest + return gott.Tuple(args), err +} + +func unmarshalGenres(args ...interface{}) (interface{}, error) { + result := args[1].(*network.Result) + genres := &Genres{} + err := json.Unmarshal(result.Body, genres) + if err != nil { + return gott.Tuple(args), err + } + + genreMap := map[int]string{} + for _, genre := range genres.Genres { + genreMap[genre.Id] = genre.Name + } + + result.Result = genreMap + return gott.Tuple(args), nil +} + +func GetGenres(language string) (map[int]string, error) { + genres, err := gott. + NewResult(gott.Tuple{&network.Request{Language: language}, &network.Result{}}). + Bind(createGenresRequest). + Bind(getCacheEntry). + Map(network.AddHeaders). + Bind(network.DoRequest). + Bind(network.HandleRequestError). + Bind(network.ReadResponse). + Tee(cleanCache). + Tee(saveCacheEntry). + Bind(unmarshalGenres). + Finish() + + if err != nil { + return map[int]string{}, err + } else { + return genres.(gott.Tuple)[1].(*network.Result).Result.(map[int]string), nil + } +} diff --git a/tmdb/serie.go b/tmdb/serie.go index 56bf0057bb7378fda661a33ff38a3a45883db5ac..2d9bc7f72a75509b1e419ec7e4136923e4058d01 100644 --- a/tmdb/serie.go +++ b/tmdb/serie.go @@ -109,6 +109,10 @@ return itemInfo } +func (s *TvSerie) GetItemType() datastructure.ItemType { + return datastructure.ItemTypeTvserie +} + func (s *TvSerie) AddBasedOn(book wikidata.Book) { s.BasedOn = book }