amuse.git

commit 90586a20af444bdfef536e0ba049dc22fb19c867

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">&#xe851;</span></li>
+						<li><a href="/users/{{.State.User.Username}}/watchlist" class="decoration-none text-accent">Watchlist</a><span class="material-icon padding-lr-_5">&#xe04a;</span></li>
+						<li><a href="/users/{{.State.User.Username}}/readlist" class="decoration-none text-accent">Readlist</a><span class="material-icon padding-lr-_5">&#xe431;</span></li>
+						<li><a href="/users/{{.State.User.Username}}/experiences" class="decoration-none text-accent">Experiences</a><span class="material-icon padding-lr-_5">&#xe042;</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">&#xe7ff;</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">&#xe408;</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">&#xe409;</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">&#xe02c;</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">&#xe408;</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">&#xe409;</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
 }