17 Commits 299b25bfbd ... 4e28fd7cad

Author SHA1 Message Date
  Adam 4e28fd7cad fix error checking in experience adding 4 years ago
  Adam 03dbd9fc1a show watched parts of film collections 4 years ago
  Adam 1ad42282c4 change progress calculation in TV series 4 years ago
  Adam 7a046688e3 simplify throwing errors in avatar 4 years ago
  Adam 7cbebeecf5 do not show skipped episodes in experiences 4 years ago
  Adam be5b4f819f update dependencies 4 years ago
  Adam 49200d8b3c show watched episodes in experiences 4 years ago
  Adam 32a3f6535e fix regression in marking episodes watched 4 years ago
  Adam 794571637c show film experiences 4 years ago
  Adam 85790d4570 show error 401 instead of 403 on empty token 4 years ago
  Adam d034e8e4e1 fix caching items in experiences 4 years ago
  Adam 90586a20af show watchlist 4 years ago
  Adam 5f8951769e add item cache from TV series 4 years ago
  Adam ceff433f65 extact datastructures 4 years ago
  Adam 78a6bc5aad add filterable item_cache 4 years ago
  Adam dbc0f4327f ellipsise overflow in random commedy placeholder 4 years ago
  Adam 829f617089 sort experiences for item 4 years ago
10 changed files with 558 additions and 42 deletions
  1. 42 0
      datastructure/experiences.go
  2. 49 0
      datastructure/item.go
  3. 31 0
      datastructure/watchlist.go
  4. 304 30
      db/db.go
  5. 10 1
      front/capnproto.go
  6. 17 0
      front/html.go
  7. 3 0
      front/renderer.go
  8. 15 11
      go.mod
  9. 87 0
      go.sum
  10. 0 0
      i18n/en-GB.toml

+ 42 - 0
datastructure/experiences.go

@@ -0,0 +1,42 @@
+package datastructure
+
+import (
+	"notabug.org/apiote/amuse/i18n"
+
+	"time"
+)
+
+type ExperiencesEntry struct {
+	ItemInfo
+	Type     string
+	Id       string
+	Code     string
+	Datetime time.Time
+}
+
+type Experiences struct {
+	List  []ExperiencesEntry
+	Page  int
+	Pages int
+	Query string
+}
+
+func (e ExperiencesEntry) FormatDatetime(strings i18n.Translation) string {
+	return i18n.FormatDate(e.Datetime, strings.Global["date_format_time"], strings.Global)
+}
+
+func (e Experiences) NextPage() int {
+	if e.Page < e.Pages {
+		return e.Page + 1
+	} else {
+		return e.Page
+	}
+}
+
+func (e Experiences) PrevPage() int {
+	if e.Page > 1 {
+		return e.Page - 1
+	} else {
+		return e.Page
+	}
+}

+ 49 - 0
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"
+)

+ 31 - 0
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
+	}
+}

+ 304 - 30
db/db.go

@@ -1,7 +1,7 @@
 package db
 
 import (
-	"notabug.org/apiote/amuse/tmdb"
+	"notabug.org/apiote/amuse/datastructure"
 	"notabug.org/apiote/amuse/utils"
 
 	"crypto/rand"
@@ -9,20 +9,18 @@ import (
 	"encoding/hex"
 	"errors"
 	"fmt"
+	"math"
 	"os"
+	"sort"
 	"time"
 
 	_ "github.com/mattn/go-sqlite3"
 )
 
-type ItemType string
-
-const (
-	ItemTypeBook    ItemType = "book"
-	ItemTypeFilm             = "film"
-	ItemTypeTvserie          = "tvserie"
-	ItemTypeUnkown           = "unknown"
-)
+type CacheEntry struct {
+	Etag string
+	Data []byte
+}
 
 type EmptyError struct {
 	message string
@@ -51,7 +49,6 @@ type Session struct {
 }
 
 func Migrate() error {
-	// todo migrations
 	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
 	if err != nil {
 		return err
@@ -59,23 +56,27 @@ func Migrate() error {
 	defer db.Close()
 
 	_, err = db.Exec(`create table cache(uri text primary key, etag text, date date, response blob, last_hit date)`)
-	if err != nil {
+	if err != nil && err.Error() != "table cache already exists" {
 		return err
 	}
 	_, err = db.Exec(`create table users(username text primary key, password text, sfa text, avatar blob, avatar_small blob, is_admin bool, recovery_codes text, timezone text)`)
-	if err != nil {
+	if err != nil && err.Error() != "table users already exists" {
 		return err
 	}
 	_, err = db.Exec(`create table sessions(id text primary key, username text, expiry datetime, is_long boolean, foreign key(username) references users(username))`)
-	if err != nil {
+	if err != nil && err.Error() != "table sessions already exists" {
 		return err
 	}
 	_, err = db.Exec(`create table wantlist(username text, item_type text, item_id text, primary key(username, item_type, item_id), foreign key(username) references users(username))`)
-	if err != nil {
+	if err != nil && err.Error() != "table wantlist already exists" {
 		return err
 	}
 	_, err = db.Exec(`create table experiences(username text, item_type text, item_id text, time datetime, foreign key(username) references users(username), primary key(username, item_type, item_id, time))`)
-	if err != nil {
+	if err != nil && err.Error() != "table experiences already exists" {
+		return err
+	}
+	_, err = db.Exec(`create table item_cache (item_type text, item_id text, cover text, status text, title text, year_start int, year_end int, based_on text, genres text, runtime int, collection int, part int, ref_count int, primary key(item_type, item_id))`)
+	if err != nil && err.Error() != "table item_cache already exists" {
 		return err
 	}
 	return nil
@@ -224,7 +225,7 @@ func ClearSessions(username string) error {
 	return nil
 }
 
-func GetItemExperiences(username, itemId string, itemType ItemType) (map[string][]time.Time, error) {
+func GetItemExperiences(username, itemId string, itemType datastructure.ItemType) (map[string][]time.Time, error) {
 	times := map[string][]time.Time{}
 	user, err := GetUser(username)
 	if err != nil {
@@ -263,10 +264,17 @@ func GetItemExperiences(username, itemId string, itemType ItemType) (map[string]
 		t = t.In(location)
 		times[id] = append(times[id], t)
 	}
+
+	for k, v := range times {
+		sort.Slice(v, func(i, j int) bool {
+			return v[i].After(v[j])
+		})
+		times[k] = v
+	}
 	return times, nil
 }
 
-func AddToExperiences(username, itemId string, itemType ItemType, datetime time.Time) error {
+func AddToExperiences(username, itemId string, itemType datastructure.ItemType, datetime time.Time) error {
 	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
@@ -284,7 +292,7 @@ func AddToExperiences(username, itemId string, itemType ItemType, datetime time.
 	return nil
 }
 
-func SkipSpecials(username, itemId string, episodesNumber int, itemType ItemType, datetime time.Time) error {
+func SkipSpecials(username, itemId string, episodesNumber int, itemType datastructure.ItemType, datetime time.Time) error {
 	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
@@ -294,6 +302,7 @@ func SkipSpecials(username, itemId string, episodesNumber int, itemType ItemType
 
 	for e := 1; e <= episodesNumber; e++ {
 		episodeId := fmt.Sprintf("%s/S00E%02d", itemId, e)
+		// todo if not watched already
 		_, err = db.Exec(`insert into experiences values(?, ?, ?, ?)`, username, itemType, episodeId, datetime)
 		if err != nil {
 			if err.Error()[:6] != "UNIQUE" {
@@ -308,7 +317,7 @@ func SkipSpecials(username, itemId string, episodesNumber int, itemType ItemType
 	return nil
 }
 
-func AddToWantList(username, itemId string, itemType ItemType) error {
+func AddToWantList(username, itemId string, itemType datastructure.ItemType) error {
 	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
@@ -324,7 +333,7 @@ func AddToWantList(username, itemId string, itemType ItemType) error {
 	return nil
 }
 
-func RemoveFromWantList(username, itemId string, itemType ItemType) error {
+func RemoveFromWantList(username, itemId string, itemType datastructure.ItemType) error {
 	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
@@ -332,15 +341,25 @@ func RemoveFromWantList(username, itemId string, itemType ItemType) error {
 	}
 	defer db.Close()
 
-	_, err = db.Exec(`delete from wantlist where username = ? and item_type = ? and item_id = ?`, username, itemType, itemId)
+	result, err := db.Exec(`delete from wantlist where username = ? and item_type = ? and item_id = ?`, username, itemType, itemId)
 	if err != nil {
-		fmt.Fprintf(os.Stderr, "Insert err %v\n", err)
+		fmt.Fprintf(os.Stderr, "Delete err %v\n", err)
+		return err
+	}
+	rows, err := result.RowsAffected()
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Delete err %v\n", err)
 		return err
 	}
+	if rows == 0 {
+		return EmptyError{
+			message: "Empty delete",
+		}
+	}
 	return nil
 }
 
-func IsOnWantList(username, itemId string, itemType ItemType) (bool, error) {
+func IsOnWantList(username, itemId string, itemType datastructure.ItemType) (bool, error) {
 	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
@@ -359,12 +378,267 @@ func IsOnWantList(username, itemId string, itemType ItemType) (bool, error) {
 	return isOnlist, nil
 }
 
-func GetItemTypeFromShow(show tmdb.Show) ItemType {
-	if _, ok := show.(*tmdb.Film); ok {
-		return ItemTypeFilm
-	} else if _, ok := show.(*tmdb.TvSerie); ok {
-		return ItemTypeTvserie
-	} else {
-		return ItemTypeUnkown
+func SaveCacheItem(itemType datastructure.ItemType, itemId string, itemInfo datastructure.ItemInfo) error {
+	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "DB open err\n")
+		return err
+	}
+	defer db.Close()
+
+	_, err = db.Exec(`insert into item_cache values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+	                  on conflict(item_type, item_id) do update set ref_count = ref_count + 1`,
+		itemType, itemId, itemInfo.Cover, itemInfo.Status, itemInfo.Title, itemInfo.YearStart, itemInfo.YearEnd, itemInfo.BasedOn, itemInfo.Genres, itemInfo.Runtime, itemInfo.Collection, itemInfo.Part, 1)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func UpdateCacheItem(itemType datastructure.ItemType, itemId string, itemInfo datastructure.ItemInfo) error {
+	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "DB open err\n")
+		return err
+	}
+	defer db.Close()
+
+	db.Exec(`update item_cache set cover = ?, status = ?, title = ?, year_start = ?, year_end = ?, based_on = ?, genres = ?, runtime = ?, collection = ?, part = ? where item_type = ? and item_id = ?`, itemInfo.Cover, itemInfo.Status, itemInfo.Title, itemInfo.YearStart, itemInfo.YearEnd, itemInfo.BasedOn, itemInfo.Genres, itemInfo.Runtime, itemInfo.Collection, itemInfo.Part, itemType, itemId)
+
+	return nil
+}
+
+func RemoveCacheItem(itemType datastructure.ItemType, itemId string) error {
+	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "DB open err\n")
+		return err
+	}
+	defer db.Close()
+
+	_, err = db.Exec(`update item_cache set ref_count = ref_count - 1 where item_id = ?`, itemId)
+
+	return err
+}
+
+func CleanItemCache() error {
+	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "DB open err\n")
+		return err
+	}
+	defer db.Close()
+
+	_, err = db.Exec(`delete from item_cache where ref_count <= 0`)
+
+	return err
+}
+
+func GetCacheItem(itemType datastructure.ItemType, itemId string) (*datastructure.ItemInfo, error) {
+	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "DB open err\n")
+		return nil, err
+	}
+	defer db.Close()
+
+	var (
+		itemInfo   datastructure.ItemInfo
+		itemTypeDb datastructure.ItemType
+		itemIdDb   string
+		refCount   int
+	)
+
+	row := db.QueryRow(`select * from cache where item_type = ? and item_id = ?`, itemType, itemId)
+
+	err = row.Scan(&itemTypeDb, &itemIdDb, &itemInfo.Cover, &itemInfo.Status, &itemInfo.Title, &itemInfo.YearStart, &itemInfo.YearEnd, &itemInfo.BasedOn, &itemInfo.Genres, &itemInfo.Runtime, &itemInfo.Collection, &itemInfo.Part, refCount)
+	if err != nil {
+		if err == sql.ErrNoRows {
+			return nil, nil
+		} else {
+			return nil, err
+		}
+	}
+	return &itemInfo, nil
+}
+
+// ====
+
+func GetCacheEntry(uri string) (*CacheEntry, error) {
+	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "DB open err\n")
+		return nil, err
+	}
+	defer db.Close()
+
+	row := db.QueryRow(`select etag, response from cache where uri = ?`, uri)
+
+	var cacheEntry CacheEntry
+	err = row.Scan(&cacheEntry.Etag, &cacheEntry.Data)
+	if err != nil {
+		if err == sql.ErrNoRows {
+			return nil, nil
+		} else {
+			return nil, err
+		}
+	}
+
+	return &cacheEntry, err
+}
+
+func CleanCache() error {
+	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "DB open err\n")
+		return err
+	}
+	defer db.Close()
+
+	row := db.QueryRow(`select count(*) from cache`)
+
+	var count int
+	err = row.Scan(&count)
+	if err != nil {
+		return err
+	}
+
+	for count > 10000 {
+		_, err = db.Exec(`delete from cache where last_update = (select min(last_update) from cache)`)
+		if err != nil {
+			return err
+		}
+		count--
+	}
+
+	return nil
+}
+
+func SaveCacheEntry(uri, etag string, data []byte) error {
+	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "DB open err\n")
+		return err
+	}
+	defer db.Close()
+
+	_, 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 // todo is not on watched
+		}
+		watchlist.List = append(watchlist.List, entry)
+	}
+
+	return watchlist, nil
+}
+
+func GetUserExperiences(username, filter string, page int) (datastructure.Experiences, error) {
+	experiences := datastructure.Experiences{}
+	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "DB open err\n")
+		return experiences, err
+	}
+	defer db.Close()
+
+	if page <= 0 {
+		page = 1
+	}
+
+	var pages float64
+	row := db.QueryRow(`select count(*) from experiences where username = ? and time != '0001-01-01 00:00:00+00:00'`, username)
+	err = row.Scan(&pages)
+	if err != nil {
+		return experiences, err
+	}
+	experiences.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 case when substr(e.item_id, 1, pos-1) = '' then e.item_id else substr(e.item_id, 1, pos-1) end as id, substr(e.item_id, pos+1) as code, e.item_type, time, title, year_start, collection, part from (select *, instr(item_id, '/') as pos from experiences) e join item_cache c on id = c.item_id and e.item_type = c.item_type where username = ? `+whereClause+` order by time desc limit ?,18;`, username, offset)
+
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Select err: %v\n", err)
+		return experiences, err
+	}
+	defer rows.Close()
+
+	for rows.Next() {
+		var (
+			entry datastructure.ExperiencesEntry
+		)
+		err := rows.Scan(&entry.Id, &entry.Code, &entry.Type, &entry.Datetime, &entry.Title, &entry.YearStart, &entry.Collection, &entry.Part)
+		entry.Part += 1
+		if err != nil {
+			fmt.Println("Scan error")
+			return datastructure.Experiences{}, err
+		}
+
+		if !entry.Datetime.IsZero() {
+			experiences.List = append(experiences.List, entry)
+		}
+	}
+
+	return experiences, nil
+
 }

+ 10 - 1
front/capnproto.go

@@ -3,6 +3,7 @@ package front
 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 @@ func (CapnprotoRenderer) RenderBook(book wikidata.Book, languages []language.Tag
 }
 
 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,11 @@ func (CapnprotoRenderer) RenderLogin(languages []language.Tag, err error, target
 	// 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)
+}
+
+func (CapnprotoRenderer) RenderExperiences(experiences datastructure.Experiences, languages []language.Tag) string {
+	return TODO("implement CapnprotoRenderer.RenderExperiences").(string)
+}

+ 17 - 0
front/html.go

@@ -6,6 +6,7 @@ import (
 	"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"
@@ -39,6 +40,10 @@ func (d RenderData) FormatDate(date time.Time) string {
 	return i18n.FormatDate(date, d.Strings.Global["date_format"], d.Strings.Global)
 }
 
+func (d RenderData) FormatDateNice(date time.Time, timezone string) string {
+	return i18n.FormatDateNice(date, d.Strings, timezone)
+}
+
 func (d RenderData) RenderAsciiDoc(s string) template.HTML {
 	return i18n.RenderAsciiDoc(s)
 }
@@ -132,3 +137,15 @@ func (HtmlRenderer) RenderLogin(languages []language.Tag, authError error, targe
 	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")
+}
+
+func (r HtmlRenderer) RenderExperiences(experiences datastructure.Experiences, languages []language.Tag) string {
+	data := RenderData{Data: experiences}
+	data.State.User = r.user
+	return render(languages, data, "experiences")
+}

+ 3 - 0
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,8 @@ type Renderer interface {
 	RenderAbout([]language.Tag) string
 	RenderErrorPage(int, []language.Tag) string
 	RenderLogin([]language.Tag, error, string) string
+	RenderWatchlist(datastructure.Watchlist, []language.Tag) string
+	RenderExperiences(datastructure.Experiences, []language.Tag) string
 }
 
 func NewRenderer(mimetype string, user accounts.User) (Renderer, error) {

+ 15 - 11
go.mod

@@ -4,22 +4,26 @@ go 1.13
 
 require (
 	github.com/BurntSushi/toml v0.3.1
-	github.com/bytesparadise/libasciidoc v0.2.0
+	github.com/alecthomas/chroma v0.7.2 // indirect
+	github.com/bytesparadise/libasciidoc v0.4.0
+	github.com/dlclark/regexp2 v1.2.0 // indirect
 	github.com/go-python/gopy v0.3.1
 	github.com/knakk/digest v0.0.0-20160404164910-fd45becddc49 // indirect
 	github.com/knakk/rdf v0.0.0-20190304171630-8521bf4c5042 // indirect
-	github.com/knakk/sparql v0.0.0-20190415133729-e66682c662f6
-	github.com/mattn/go-sqlite3 v2.0.2+incompatible
-	github.com/onsi/ginkgo v1.10.3 // indirect
+	github.com/knakk/sparql v0.0.0-20191213045353-fd0bd0e76475
+	github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
+	github.com/mattn/go-sqlite3 v2.0.3+incompatible
+	github.com/onsi/ginkgo v1.12.0 // indirect
 	github.com/onsi/gomega v1.7.1 // indirect
+	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pquerna/otp v1.2.0
-	github.com/sirupsen/logrus v1.4.2 // indirect
+	github.com/sirupsen/logrus v1.5.0 // indirect
 	github.com/stretchr/testify v1.4.0 // indirect
-	golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
-	golang.org/x/net v0.0.0-20191116160921-f9c825593386 // indirect
-	golang.org/x/text v0.3.0
-	golang.org/x/tools v0.0.0-20191204011308-9611592c72f6 // indirect
-	gopkg.in/yaml.v2 v2.2.7 // indirect
-	notabug.org/apiote/gott v1.1.0
+	golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5
+	golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f // indirect
+	golang.org/x/text v0.3.2
+	golang.org/x/tools v0.0.0-20200423205358-59e73619c742 // indirect
+	gopkg.in/yaml.v2 v2.2.8 // indirect
+	notabug.org/apiote/gott v1.1.2
 	zombiezen.com/go/capnproto2 v2.17.0+incompatible
 )

+ 87 - 0
go.sum

@@ -1,83 +1,170 @@
 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
+github.com/alecthomas/chroma v0.7.1 h1:G1i02OhUbRi2nJxcNkwJaY/J1gHXj9tt72qN6ZouLFQ=
+github.com/alecthomas/chroma v0.7.1/go.mod h1:gHw09mkX1Qp80JlYbmN9L3+4R5o6DJJ3GRShh+AICNc=
+github.com/alecthomas/chroma v0.7.2 h1:B76NU/zbQYIUhUowbi4fmvREmDUJLsUzKWTZmQd3ABY=
+github.com/alecthomas/chroma v0.7.2/go.mod h1:fv5SzZPFJbwp2NXJWpFIX7DZS4HgV1K4ew4Pc2OZD9s=
+github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
+github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI=
+github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
 github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
 github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
 github.com/bytesparadise/libasciidoc v0.2.0 h1:W+Yh4cXehuQvFA+Ncs4tIgwBXiH8ie+KhHmMXkBhIcc=
 github.com/bytesparadise/libasciidoc v0.2.0/go.mod h1:CZX8GIEkxy/LHrDZjPbNrE16RQFDrnG6hBjnjXcD34Y=
+github.com/bytesparadise/libasciidoc v0.4.0 h1:fse9nKBTZ1OcAltOhf5XJUxctakbiaDT3Jw6qCPaM7Y=
+github.com/bytesparadise/libasciidoc v0.4.0/go.mod h1:fNxeS06tJufiBEyZJXnO0ng4xv8EdlswK/tKStNz/MA=
+github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
+github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dlclark/regexp2 v1.1.6 h1:CqB4MjHw0MFCDj+PHHjiESmHX+N7t0tJzKvC6M97BRg=
+github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
+github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
+github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/go-python/gopy v0.3.1 h1:l0zBAjU89xGoFBR12NTK+JGj6O2dCqRB/rDTN44APBY=
 github.com/go-python/gopy v0.3.1/go.mod h1:gQ2Itc84itA1AjrVqnMnv7HLkfmNObRXlR1co7CXpbk=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/gonuts/commander v0.1.0 h1:EcDTiVw9oAVORFjQOEOuHQqcl6OXMyTgELocTq6zJ0I=
 github.com/gonuts/commander v0.1.0/go.mod h1:qkb5mSlcWodYgo7vs8ulLnXhfinhZsZcm6+H/z1JjgY=
 github.com/gonuts/flag v0.1.0 h1:fqMv/MZ+oNGu0i9gp0/IQ/ZaPIDoAZBOBaJoV7viCWM=
 github.com/gonuts/flag v0.1.0/go.mod h1:ZTmTGtrSPejTo/SRNhCqwLTmiAgyBdCkLYhHrAoBdz4=
 github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/knakk/digest v0.0.0-20160404164910-fd45becddc49 h1:P6Mw09IOeKKS4klYhjzHzaEx2RcNshynjfDhzCQ8BoE=
 github.com/knakk/digest v0.0.0-20160404164910-fd45becddc49/go.mod h1:dQr9I8Xw26daWGE/crxUleRxmpFI5uhfedWqRNHHq0c=
 github.com/knakk/rdf v0.0.0-20190304171630-8521bf4c5042 h1:Vzdm5hdlLdpJOKK+hKtkV5u7xGZmNW6aUBjGcTfwx84=
 github.com/knakk/rdf v0.0.0-20190304171630-8521bf4c5042/go.mod h1:fYE0718xXI13XMYLc6iHtvXudfyCGMsZ9hxSM1Ommpg=
 github.com/knakk/sparql v0.0.0-20190415133729-e66682c662f6 h1:/9NsggFoqFNblbAcHDeeAX9tiYnT6TteCUS80zanCGA=
 github.com/knakk/sparql v0.0.0-20190415133729-e66682c662f6/go.mod h1:vxUbHrxs7JHQF6LITj9Rp9yf2bqyz+5JZzPZkEkS3MA=
+github.com/knakk/sparql v0.0.0-20191213045353-fd0bd0e76475 h1:J75ktE0AJuhyTqS6V8cBHNLeCEv5XbW58g9r3Zpyz4k=
+github.com/knakk/sparql v0.0.0-20191213045353-fd0bd0e76475/go.mod h1:vxUbHrxs7JHQF6LITj9Rp9yf2bqyz+5JZzPZkEkS3MA=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
+github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kuangchanglang/graceful v1.0.0 h1:EPcA4vV75CkLi9+tW1+cd6KpfULYRTxTm1MO8USa49k=
 github.com/kuangchanglang/graceful v1.0.0/go.mod h1:fQkb+p3PRjvdiAsa65Qv78lm9CsYc4M+yhiuU1rOVtg=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 github.com/mattn/go-sqlite3 v2.0.2+incompatible h1:qzw9c2GNT8UFrgWNDhCTqRqYUSmu/Dav/9Z58LGpk7U=
 github.com/mattn/go-sqlite3 v2.0.2+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
+github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mna/pigeon v1.0.1-0.20190909211542-7ee56e19b15c h1:QRaadf9Fu8xAfNDS8PvaM0VmY2FnYHlddtnIExKj68k=
+github.com/mna/pigeon v1.0.1-0.20190909211542-7ee56e19b15c/go.mod h1:rkFeDZ0gc+YbnrXPw0q2RlI0QRuKBBPu67fgYIyGRNg=
+github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5 h1:8Q0qkMVC/MmWkpIdlvZgcv2o2jrlF6zqVOh7W5YHdMA=
+github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY=
 github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU=
+github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
+github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ=
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pquerna/otp v1.2.0 h1:/A3+Jn+cagqayeR3iHs/L62m5ue7710D35zl1zJ1kok=
 github.com/pquerna/otp v1.2.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
+github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q=
+github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
+github.com/sozorogami/gover v0.0.0-20171022184752-b58185e213c5 h1:TAPeDBsd52dRWoWzf5trgBzxzMYHTYjYI+4xNyCdoCU=
+github.com/sozorogami/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:nHNlDYIQZn44RvqH0kCpl/dMMVWXkav0QIgzGxV1Ab4=
+github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200420201142-3c4aac89819a h1:y6sBfNd1b9Wy08a6K1Z1DZc4aXABUN5TKjkYhz7UKmo=
+golang.org/x/crypto v0.0.0-20200420201142-3c4aac89819a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5 h1:Q7tZBpemrlsc2I7IyODzhtallWRSm4Q0d09pL6XbQtU=
+golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190909003024-a7b16738d86b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20191116160921-f9c825593386 h1:ktbWvQrW08Txdxno1PiDpSxPXG6ndGsfnJjRRtkM0LQ=
 golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190910064555-bbd175535a8b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f h1:gWF768j/LaZugp8dyS4UwsslYCYz9XgFxvlgsn0n9H8=
+golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190830223141-573d9926052a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191204011308-9611592c72f6 h1:BP62y4oUl8+/CvHuvVqHIPmVRixgDl6y6a+tR7pXXIA=
 golang.org/x/tools v0.0.0-20191204011308-9611592c72f6/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191220234730-f13409bbebaf h1:K7C8vSrr0PeD/cgNkkjpByDFJqzjr2YDmm3VPRjGfJM=
+golang.org/x/tools v0.0.0-20191220234730-f13409bbebaf/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200422022333-3d57cf2e726e h1:3Dzrrxi54Io7Aoyb0PYLsI47K2TxkRQg+cqUn+m04do=
+golang.org/x/tools v0.0.0-20200422022333-3d57cf2e726e/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200423205358-59e73619c742 h1:9OGWpORUXvk8AsaBJlpzzDx7Srv/rSK6rvjcsJq4rJo=
+golang.org/x/tools v0.0.0-20200423205358-59e73619c742/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
 gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 notabug.org/apiote/gott v1.0.1 h1:yfq2z3WM0lYFSu6xFvh1sWBKgg6yaXwF9/2wqJiKky8=
 notabug.org/apiote/gott v1.0.1/go.mod h1:Z9hFvCdzZkFSegBkLa6n0X6AuUiw2BwgG4MFLgBMjD4=
 notabug.org/apiote/gott v1.1.0 h1:RGGbJo9ON5Qsk/lsw0oF1tiyFeogORINGILqizbdkC8=
 notabug.org/apiote/gott v1.1.0/go.mod h1:Z9hFvCdzZkFSegBkLa6n0X6AuUiw2BwgG4MFLgBMjD4=
+notabug.org/apiote/gott v1.1.1 h1:BFKdZnZPCTZa8BrEGVSmMYhkgXD30aR9JBPcxMw1Rnc=
+notabug.org/apiote/gott v1.1.1/go.mod h1:Z9hFvCdzZkFSegBkLa6n0X6AuUiw2BwgG4MFLgBMjD4=
+notabug.org/apiote/gott v1.1.2 h1:Z22X9/8XrK5M5oARoE2fh3sJGPAJ84GuyGg2nKOjweQ=
+notabug.org/apiote/gott v1.1.2/go.mod h1:Z9hFvCdzZkFSegBkLa6n0X6AuUiw2BwgG4MFLgBMjD4=
 zombiezen.com/go/capnproto2 v2.17.0+incompatible h1:sIoKPFGNlM38Qh+PBLa9Wzg1j99oInS/Qlk+5N/CHa4=
 zombiezen.com/go/capnproto2 v2.17.0+incompatible/go.mod h1:XO5Pr2SbXgqZwn0m0Ru54QBqpOf4K5AYBO+8LAOBQEQ=

+ 0 - 0
i18n/en-GB.toml


Some files were not shown because too many files changed in this diff