amuse.git

commit ec0097a6dd4814e9304bd5f38513714a6add7395

Author: Adam <git@apiote.tk>

Merge branch 'v0.4.0'

 CHANGELOG.adoc | 96 ++
 Dockerfile | 16 
 README.adoc | 10 
 config/config.go | 55 +
 datastructure/book.go | 71 ++
 datastructure/item.go | 1 
 datastructure/readlist.go | 39 +
 db/db.go | 164 ++++-
 example.toml | 5 
 front/capnproto.go | 6 
 front/html.go | 32 
 front/renderer.go | 3 
 go.mod | 3 
 go.sum | 46 -
 i18n/en-GB.toml | 17 
 i18n/i18n.go | 76 ++
 i18n/pl-PL.toml | 4 
 inventaire/book.go | 206 ++++++
 libamuse/account.go | 35 
 libamuse/book.go | 39 +
 libamuse/bookserie.go | 11 
 libamuse/common.go | 25 
 libamuse/manage.go | 29 
 libamuse/readlist.go | 54 +
 libamuse/serie.go | 60 +
 libamuse/user.go | 53 -
 main.go | 43 +
 mkfile | 13 
 network/common.go | 2 
 protocol/amuse.proto | 73 --
 protocol/amuse.proto.go | 1126 --------------------------------------
 protocol/film.go | 100 ---
 protocol/makefile | 2 
 router.go | 66 +
 router.py | 122 ----
 static/img/avatar.svg | 14 
 static/style/style.css | 51 +
 templates/about.html | 2 
 templates/book.html | 40 +
 templates/bookserie.html | 10 
 templates/experiences.html | 3 
 templates/film.html | 30 
 templates/index.html | 6 
 templates/login.html | 2 
 templates/person.html | 22 
 templates/readlist.html | 116 +++
 templates/search.html | 30 
 templates/signup_locked.html | 32 +
 templates/tvqueue.html | 16 
 templates/tvserie.html | 67 +
 templates/watchlist.html | 16 
 tmdb/common.go | 8 
 tmdb/film.go | 20 
 tmdb/genres.go | 3 
 tmdb/person.go | 75 +
 tmdb/random.go | 3 
 tmdb/search.go | 19 
 tmdb/serie.go | 121 ++-
 utils/config.go | 3 
 utils/error.go | 9 
 wikidata/book.go | 62 -
 wikidata/bookserie.go | 26 
 wikidata/common.go | 13 
 wikidata/search.go | 17 


diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc
index 8125da169f7f931ea44ab97c41fe7a5f6c88f2cb..4c3269efc61befb3a02620e7211666f86d0a60e8 100644
--- a/CHANGELOG.adoc
+++ b/CHANGELOG.adoc
@@ -9,26 +9,90 @@ == Unreleased
 
 === Added
 
+* Account page (avatar, timezone, import, export) [0.5]
+* Experience deletion [0.5]
+* Wantlist deletion [0.5]
+* CLI create account [0.5]
+* CLI reset password [0.5]
+* Experienced, to-experience, TV serie watching in search results [0.5]
+* Episode page (cast) [0.5]
+* Lists filtering [0.5]
+* Link page -> experience [0.5]
+* Return to episode after watching [0.5]
+* check if wd:Q… is a book/~series [0.6]
+* 'Read book first' in lists [0.6]
+* Cache control [0.8]
+* Cache to webp and srcset images [0.8]
+* Kobo server [0.9]
+* mpv server [0.9]
+* man pages [0.10]
+* rtl [0.10]
+* API [0.10]
+
+=== Changed
+
+* cast/crew in tvSeries with season numbers [0.5]
+* Better error showing [0.5]
+* 'Based on' overhaul [0.6]
+* css minification + gzip (^) [0.7]
+* libamuse removal (plus refactoring) [0.7]
+
+=== Fixed
+
+* properly paginate inventaire results, sort in one page (some relevance?) [0.6]
+* (Emphasise book series) [0.6]
+* Gneral layout improvement [0.7]
+* Light theme improvement [0.7]
+* Translation (plurals) [0.7]
+* proper state when back, refreshed log-in, refreshed watched [0.8]
+* a11n, screanreaders [0.10]
+
+=== Removed
+
+* Remove admin [0.10]
+
+
+== [0.4.0] — 2020-11-25
+
+=== Added
+
+* Public registrations can be blocked
+* Books from Inventaire can be viewed
 * Readlist
-* Experience logging (read)
-* Upcoming episodes and films from watchlist
-* Film and series discovery
-* Manpages
-* Verify wd:Q… items
-* Tv series episode page (with episode cast and experiences)
-* a11n
-* RTL
-* Nigmas (search filters)
-* API
-* Read position sync (Koreader integration)
-* Watch position sycn (mpv integration)
+* Books experiences
+* Films and TV series can be periodically updated
+* Whole seasons can be watched at once
+* Last episode to air shows episode code
+* Spoilers are blurred
+* Runtime in watchlist is shown
+* Default language and TMDB API key presence is validated at start
+* Cancelled films are marked in watchlist
+* Lists show ‘Page i/N’
 
 === Changed
 
-* Search pagination based on films *and* books
-* Show books not available in Wikidata, from Inventaire
-* Further optimisation (minify css, caching)
-* Improve based-on
+* Search box on main page is auto-focused
+* Person backdrop is their most popular film
+* Empty user’s avatar is static
+* Remaining progress in TV series is visible grey
+* Moved TMDB API key to config file
+* All image lists look the same
+* Places for images have minimum size (content jumps less)
+
+=== Fixed
+
+* On network failure search results are empty
+* Typo on login page
+* Input backgrounds in webkit
+* Book data is shown in native language if label is empty
+* Watch buttons are shown only for logged users
+* Fall back if accept-language is unknown
+* Other time fields are visible even when empty (when browsers do not support them)
+* Minor bugfixes
+
+=== Removed
+
+* Python router
 
 == [0.3.0] — 2020-05-23
 




diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index cfe02e964c4366fb138d84e26feee8b43989c2a8..0000000000000000000000000000000000000000
--- a/Dockerfile
+++ /dev/null
@@ -1,16 +0,0 @@
-FROM ubuntu:latest
-
-RUN apt update && apt install -y software-properties-common curl
-RUN add-apt-repository -y ppa:longsleep/golang-backports && apt update && apt install -y golang-go
-RUN apt install -y python3.7 python3.7-dev python3-pip git 9base
-RUN sed 's/python3/python3.7/' /usr/bin/pip3 > pip3 && mv pip3 /usr/bin/pip3
-RUN chmod 755 /usr/bin/pip3
-
-RUN pip3 install pybindgen
-RUN go get golang.org/x/tools/cmd/goimports
-RUN go get github.com/go-python/gopy
-
-RUN ln -s /usr/bin/python3.7 /usr/bin/python
-RUN ln -s /usr/bin/python3.7-config /usr/bin/python-config
-
-ENV PATH="/root/go/bin:${PATH}:/usr/lib/plan9/bin"




diff --git a/README.adoc b/README.adoc
index 4c4e0c57cb2c40ee7e79036cf8ce63c6a5b5cea9..8d30da2ddab0d80c2a93a5588addaf99b4bd4ee3 100644
--- a/README.adoc
+++ b/README.adoc
@@ -1,6 +1,6 @@
 = a·muse
 apiote <me@apiote.tk>
-v0.3.0 (Colin) 2020-03-23
+v0.4.0 (Dish of the Day) 2020-11-25
 :toc:
 
 a·muse is a no-JavaScript frontend for The Movie Database. It is also a system that connects films with books which the former are based on, thanks to using data from Wikidata. Finally, a·muse is also a place to collect ideas which films, books, and series watch or read next, and which of those have already been watched or read.
@@ -24,15 +24,15 @@ * `mk`
 
 Then, all You have to do is run `mk`, and—optionally—`mk install`
 
-=== Python router
+== Configuration
 
-WARNING: Python router is deprecated
+a·muse can be configured with command line arguments or with configuration file. Example configuration file can be found in `example.toml`. The only mandatory field is `TmdbApiKey`
 
 == Contribute
 
-Contributions are welcome either as merge requests or patches (send patches to `git@apiote.tk`).
+Contributions are welcome either as patches (send patches to `git@apiote.tk`).
 
-If You want to translate a·muse, translate strings in `i18n/default.toml` and request merge, send patch, or share the translated file in any other way. The translated file must be named `tag.toml`, where `tag` is the language tag of the translation. Translations don’t have to be full; a·muse falls back to default (en-GB) when strings are missing.
+If You want to translate a·muse, translate strings in `i18n/default.toml` and send patch, or share the translated file in any other way. The translated file must be named `tag.toml`, where `tag` is the language tag of the translation. Translations don’t have to be full; a·muse falls back to default (en-GB) when strings are missing.
 
 This project uses https://nvie.com/posts/a-successful-git-branching-model/[nvie’s branching model]. Please request merges to `develop` branch.
 




diff --git a/config/config.go b/config/config.go
new file mode 100644
index 0000000000000000000000000000000000000000..211b7ffd335038aa394eb27b30521a83368ba8cd
--- /dev/null
+++ b/config/config.go
@@ -0,0 +1,55 @@
+package config
+
+import (
+	"bufio"
+	"fmt"
+	"os"
+	"strings"
+)
+
+var (
+	OpenRegistration      = false
+	DataHome              = "/usr/local/amuse"
+	Port             uint = 5008
+	Address               = "127.0.0.1"
+	TmdbApiKey            = ""
+)
+
+func ReadConfig(path string) error {
+	file, err := os.Open(path)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return nil
+		} else {
+			fmt.Printf("error opening configuration %v\n", err)
+			return err
+		}
+	}
+	defer file.Close()
+
+	scanner := bufio.NewScanner(file)
+	for scanner.Scan() {
+		line := scanner.Text()
+		assignment := strings.Split(line, "=")
+		variable := strings.Trim(assignment[0], " ")
+		value := strings.Trim(assignment[1], " ")
+		switch variable {
+		case "OpenRegistration":
+			OpenRegistration = value == "true"
+		case "DataHome":
+			DataHome = value
+		case "Address":
+			Address = value
+		case "TmdbApiKey":
+			TmdbApiKey = value
+		case "Port":
+			fmt.Sscanf(value, "%d", &Port)
+		}
+	}
+
+	if err := scanner.Err(); err != nil {
+		fmt.Printf("error reading configuration %v\n", err)
+		return err
+	}
+	return nil
+}




diff --git a/datastructure/book.go b/datastructure/book.go
new file mode 100644
index 0000000000000000000000000000000000000000..8b4e0b201d0a48fad7cd32a5583be0c26617fc44
--- /dev/null
+++ b/datastructure/book.go
@@ -0,0 +1,71 @@
+package datastructure
+
+import (
+	"notabug.org/apiote/amuse/i18n"
+
+	"strings"
+	"time"
+)
+
+type Source struct {
+	Url  string
+	Name string
+}
+
+type Work interface {
+	GetArticle() string
+	SetDescription(description string)
+}
+
+type Book struct {
+	Id           string
+	Uri          string
+	Source       []Source
+	Cover        string
+	Authors      []string
+	Year         int64
+	Title        string
+	SerieUri     string
+	SerieName    string
+	PartInSerie  string
+	Genres       []string
+	Article      string
+	Description  string
+	IsOnWantList bool
+	Experiences  []time.Time
+}
+
+func (b Book) GetArticle() string {
+	return b.Article
+}
+
+func (b *Book) SetDescription(description string) {
+	if b.Description == "" {
+		b.Description = description
+	}
+}
+
+func (b *Book) GetItemInfo() ItemInfo {
+	return ItemInfo{
+		Cover:     b.Cover,
+		Title:     b.Title,
+		YearStart: int(b.Year),
+		Genres:    strings.Join(b.Genres, ", "),
+	}
+}
+
+func (b *Book) GetItemType() ItemType {
+	return ItemTypeBook
+}
+
+func (b *Book) SetOnWantList(isOnList bool) {
+	b.IsOnWantList = isOnList
+}
+
+func (b Book) GetLastExperienceFull(strings i18n.Translation) string {
+	return i18n.FormatDate(b.Experiences[0], strings.Global["date_format_full"], strings.Global)
+}
+
+func (b Book) GetLastExperience(strings i18n.Translation, timezone string) string {
+	return i18n.FormatDateNice(b.Experiences[0], strings, timezone)
+}




diff --git a/datastructure/item.go b/datastructure/item.go
index 49049f609c6edc1ded63cd8fa5abdb1417e47353..fc6dd7d5be64cce5ddc32638ff8f6b48aad3a0cd 100644
--- a/datastructure/item.go
+++ b/datastructure/item.go
@@ -41,6 +41,7 @@
 type Item interface {
 	GetItemInfo() ItemInfo
 	GetItemType() ItemType
+	SetOnWantList(isOnList bool)
 }
 
 type ItemType string




diff --git a/datastructure/readlist.go b/datastructure/readlist.go
new file mode 100644
index 0000000000000000000000000000000000000000..423ba9ba732329e6eff866e4634cc451f350615a
--- /dev/null
+++ b/datastructure/readlist.go
@@ -0,0 +1,39 @@
+package datastructure
+
+type ReadlistEntry struct {
+	ItemInfo
+	Id          string
+	HasPrevious bool
+}
+
+type Readlist struct {
+	List   []ReadlistEntry
+	Page   int
+	Pages  int
+	Genres map[int]string
+	Query  string
+}
+
+func (w *Readlist) SetGenres(m map[int]string) {
+	w.Genres = m
+}
+
+func (w *Readlist) GetType() ItemType {
+	return ItemTypeBook
+}
+
+func (w Readlist) NextPage() int {
+	if w.Page < w.Pages {
+		return w.Page + 1
+	} else {
+		return w.Page
+	}
+}
+
+func (w Readlist) 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 a9ebc7135c0c3a8d70f40840f84d62d2aca6f413..d1bd3e7d5a51b61dc8861241e5e80e9f63121415 100644
--- a/db/db.go
+++ b/db/db.go
@@ -2,7 +2,7 @@ package db
 
 import (
 	"notabug.org/apiote/amuse/datastructure"
-	"notabug.org/apiote/amuse/utils"
+	"notabug.org/apiote/amuse/config"
 
 	"crypto/rand"
 	"database/sql"
@@ -49,7 +49,7 @@ 	IsLong   bool
 }
 
 func Migrate() error {
-	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	db, err := sql.Open("sqlite3", config.DataHome+"/amuse.db")
 	if err != nil {
 		return err
 	}
@@ -83,7 +83,7 @@ 	return nil
 }
 
 func MakeAdmin(username string) error {
-	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	db, err := sql.Open("sqlite3", config.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
 		return err
@@ -116,7 +116,7 @@ 	return nil
 }
 
 func InsertUser(username, password, sfaSecret, recoveryCodes string) error {
-	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	db, err := sql.Open("sqlite3", config.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
 		return err
@@ -132,7 +132,7 @@ 	return nil
 }
 
 func GetUser(username string) (*User, error) {
-	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	db, err := sql.Open("sqlite3", config.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
 		return nil, err
@@ -157,7 +157,7 @@ 	return &user, nil
 }
 
 func UpdateRecoveryCodes(username, recoveryCodes string) error {
-	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	db, err := sql.Open("sqlite3", config.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
 		return err
@@ -177,7 +177,7 @@ 	sessionIdRaw := make([]byte, 64)
 	rand.Read(sessionIdRaw)
 	sessionId := hex.EncodeToString(sessionIdRaw)
 
-	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	db, err := sql.Open("sqlite3", config.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
 		return Session{}, err
@@ -199,7 +199,7 @@ 	return Session{Id: sessionId, Username: username, IsLong: long}, nil
 }
 
 func GetSession(token string) (*Session, error) {
-	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	db, err := sql.Open("sqlite3", config.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
 		return nil, err
@@ -225,7 +225,7 @@ 	return &session, nil
 }
 
 func ClearSessions(username string) error {
-	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	db, err := sql.Open("sqlite3", config.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
 		return err
@@ -242,7 +242,7 @@ 	return nil
 }
 
 func RemoveSession(username, token string) error {
-	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	db, err := sql.Open("sqlite3", config.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
 		return err
@@ -273,7 +273,7 @@ 		fmt.Fprintf(os.Stderr, "Load location err: %v\n", err)
 		return times, err
 	}
 
-	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	db, err := sql.Open("sqlite3", config.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
 		return times, err
@@ -310,7 +310,7 @@ 	return times, nil
 }
 
 func AddToExperiences(username, itemId string, itemType datastructure.ItemType, datetime time.Time) (int, error) {
-	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	db, err := sql.Open("sqlite3", config.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
 		return 0, err
@@ -373,8 +373,8 @@
 	return int(insertedRowsNumber - deletedRowsNumber), nil
 }
 
-func SkipSpecials(username, itemId string, episodes []string, itemType datastructure.ItemType) (int, error) {
-	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+func WatchWholeSerie(username, itemId string, episodes []string, itemType datastructure.ItemType, datetime time.Time) (int, error) {
+	db, err := sql.Open("sqlite3", config.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
 		return 0, err
@@ -412,7 +412,7 @@ 	for _, episodeId := range episodes {
 		if watched[episodeId] > 0 {
 			continue
 		}
-		_, err = tx.Exec(`insert into experiences values(?, ?, ?, ?)`, username, itemType, episodeId, "0001-01-01 00:00:00+00:00")
+		_, err = tx.Exec(`insert into experiences values(?, ?, ?, ?)`, username, itemType, episodeId, datetime)
 		if err != nil {
 			if err.Error()[:6] != "UNIQUE" {
 				fmt.Fprintf(os.Stderr, "Insert err %v\n", err)
@@ -434,7 +434,7 @@ 	return modifiedRows, nil
 }
 
 func ClearSpecials(username, itemId string, episodes []string, itemType datastructure.ItemType) error {
-	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	db, err := sql.Open("sqlite3", config.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
 		return err
@@ -490,7 +490,7 @@ 	return nil
 }
 
 func AddToWantList(username, itemId string, itemType datastructure.ItemType) error {
-	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	db, err := sql.Open("sqlite3", config.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
 		return err
@@ -506,7 +506,7 @@ 	return nil
 }
 
 func RemoveFromWantList(username, itemId string, itemType datastructure.ItemType) error {
-	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	db, err := sql.Open("sqlite3", config.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
 		return err
@@ -532,7 +532,7 @@ 	return nil
 }
 
 func IsOnWantList(username, itemId string, itemType datastructure.ItemType) (bool, error) {
-	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	db, err := sql.Open("sqlite3", config.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
 		return false, err
@@ -555,7 +555,7 @@ 	if refs == 0 {
 		return nil
 	}
 
-	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	db, err := sql.Open("sqlite3", config.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
 		return err
@@ -572,7 +572,7 @@ 	return nil
 }
 
 func UpdateCacheItem(itemType datastructure.ItemType, itemId string, itemInfo datastructure.ItemInfo) error {
-	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	db, err := sql.Open("sqlite3", config.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
 		return err
@@ -585,7 +585,7 @@ 	return nil
 }
 
 func RemoveCacheItem(itemType datastructure.ItemType, itemId string) error {
-	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	db, err := sql.Open("sqlite3", config.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
 		return err
@@ -598,7 +598,7 @@ 	return err
 }
 
 func CleanItemCache() error {
-	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	db, err := sql.Open("sqlite3", config.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
 		return err
@@ -611,7 +611,7 @@ 	return err
 }
 
 func GetCacheItem(itemType datastructure.ItemType, itemId string) (*datastructure.ItemInfo, error) {
-	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	db, err := sql.Open("sqlite3", config.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
 		return nil, err
@@ -641,7 +641,7 @@
 // ====
 
 func GetCacheEntry(uri string) (*CacheEntry, error) {
-	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	db, err := sql.Open("sqlite3", config.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
 		return nil, err
@@ -664,7 +664,7 @@ 	return &cacheEntry, err
 }
 
 func CleanCache() error {
-	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	db, err := sql.Open("sqlite3", config.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
 		return err
@@ -691,7 +691,7 @@ 	return nil
 }
 
 func SaveCacheEntry(uri, etag string, data []byte) error {
-	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	db, err := sql.Open("sqlite3", config.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
 		return err
@@ -705,7 +705,7 @@ }
 
 func GetWatchlist(username, filter string, page int) (datastructure.Watchlist, error) {
 	watchlist := datastructure.Watchlist{}
-	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	db, err := sql.Open("sqlite3", config.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
 		return watchlist, err
@@ -716,14 +716,72 @@ 	if page <= 0 {
 		page = 1
 	}
 
+	offset := (page - 1) * 18
+
+	//todo filter, order by
+
+	var whereClause string
+	if filter != "" {
+		whereClause = "and c1.title like '%" + filter + "%'"
+	}
+
 	var pages float64
-	row := db.QueryRow(`select count(*) from wantlist where item_type = 'film' and username = ?`, username)
+	row := db.QueryRow(`select distinct count(*) from wantlist w natural join item_cache c1 where c1.item_type = 'film' and w.username = ? `+whereClause, username)
 	err = row.Scan(&pages)
 	if err != nil {
 		return watchlist, err
 	}
 	watchlist.Pages = int(math.Ceil(pages / 18))
 
+	rows, err := db.Query(`select distinct 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 w natural join item_cache c1) left join (experiences e natural join item_cache c2) on(c1.part-1 = c2.part and c1.collection = c2.collection and e.username = w.username) where c1.item_type = 'film' and w.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 entry.Part > 0 && prevPart == nil {
+			entry.HasPrevious = true
+		}
+		watchlist.List = append(watchlist.List, entry)
+	}
+
+	return watchlist, nil
+}
+
+func GetReadlist(username, filter string, page int) (datastructure.Readlist, error) {
+	readlist := datastructure.Readlist{}
+	db, err := sql.Open("sqlite3", config.DataHome+"/amuse.db")
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "DB open err\n")
+		return readlist, err
+	}
+	defer db.Close()
+
+	if page <= 0 {
+		page = 1
+	}
+
+	var pages float64
+	row := db.QueryRow(`select count(*) from wantlist where item_type = 'book' and username = ?`, username)
+	err = row.Scan(&pages)
+	if err != nil {
+		return readlist, err
+	}
+	readlist.Pages = int(math.Ceil(pages / 18))
+
 	offset := (page - 1) * 18
 
 	//todo filter, order by
@@ -733,37 +791,37 @@ 	if filter != "" {
 		whereClause = "and c1.title like '%" + filter + "%'"
 	}
 
-	rows, err := db.Query(`select distinct 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 w natural join item_cache c1) left join (experiences e natural join item_cache c2) on(c1.part-1 = c2.part and c1.collection = c2.collection and e.username = w.username) where c1.item_type = 'film' and w.username = ? `+whereClause+` order by c1.title limit ?,18`, username, offset)
+	rows, err := db.Query(`select distinct 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 w natural join item_cache c1) left join (experiences e natural join item_cache c2) on(c1.part-1 = c2.part and c1.collection = c2.collection and e.username = w.username) where c1.item_type = 'book' and w.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
+		return readlist, err
 	}
 	defer rows.Close()
 
 	for rows.Next() {
 		var (
-			entry    datastructure.WatchlistEntry
+			entry    datastructure.ReadlistEntry
 			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
+			return datastructure.Readlist{}, err
 		}
 
 		if entry.Part > 0 && prevPart == nil {
 			entry.HasPrevious = true
 		}
-		watchlist.List = append(watchlist.List, entry)
+		readlist.List = append(readlist.List, entry)
 	}
 
-	return watchlist, nil
+	return readlist, nil
 }
 
 func GetTvQueue(username, filter string, page int) (datastructure.TvQueue, error) {
 	tvQueue := datastructure.TvQueue{}
-	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	db, err := sql.Open("sqlite3", config.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
 		return tvQueue, err
@@ -844,9 +902,41 @@
 	return tvQueue, nil
 }
 
+func GetWantlistUris() ([]string, error) {
+	uris := []string{}
+	db, err := sql.Open("sqlite3", config.DataHome+"/amuse.db")
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "DB open err\n")
+		return uris, err
+	}
+	defer db.Close()
+
+	rows, err := db.Query(`select item_type, item_id from wantlist where item_type in ('film', 'tvserie')`)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Select err: %v\n", err)
+		return uris, err
+	}
+	defer rows.Close()
+	
+	for rows.Next() {
+		var (
+			itemType string
+			itemId string
+		)
+		err := rows.Scan(&itemType, &itemId)
+		if err != nil {
+			fmt.Println("Scan error")
+			return uris, err
+		}
+		uris = append(uris, itemType+"/"+itemId)
+	}
+	return uris, nil
+}
+
+
 func GetUserExperiences(username, filter string, page int) (datastructure.Experiences, error) {
 	experiences := datastructure.Experiences{}
-	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	db, err := sql.Open("sqlite3", config.DataHome+"/amuse.db")
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "DB open err\n")
 		return experiences, err




diff --git a/example.toml b/example.toml
new file mode 100644
index 0000000000000000000000000000000000000000..28f42f0d398c4d8addb23d1bb676782c8308c27d
--- /dev/null
+++ b/example.toml
@@ -0,0 +1,5 @@
+OpenRegistration = true
+Address = 127.0.0.1
+Port = 5008
+DataHome = /usr/share/amuse
+TmdbApiKey = deadbeef




diff --git a/front/capnproto.go b/front/capnproto.go
index d722f33eac2fc1462da7af45894dc824575f6699..aa36f5d2c5a4b4df199aa4021eb4070e959e47c8 100644
--- a/front/capnproto.go
+++ b/front/capnproto.go
@@ -36,7 +36,7 @@ func (CapnprotoRenderer) RenderPerson(person *tmdb.Person, languages []language.Tag) string {
 	return TODO("implement CapnprotoRenderer.RenderPerson").(string)
 }
 
-func (CapnprotoRenderer) RenderBook(book wikidata.Book, languages []language.Tag) string {
+func (CapnprotoRenderer) RenderBook(book datastructure.Book, languages []language.Tag) string {
 	return TODO("implement CapnprotoRenderer.RenderBook").(string)
 }
 func (CapnprotoRenderer) RenderBookSerie(bookSerie wikidata.BookSerie, languages []language.Tag) string {
@@ -77,6 +77,10 @@ }
 
 func (CapnprotoRenderer) RenderTvQueue(watchlist datastructure.TvQueue, languages []language.Tag) string {
 	return TODO("implement CapnprotoRenderer.RenderTvQueue").(string)
+}
+
+func (CapnprotoRenderer) RenderReadlist(readlist datastructure.Readlist, languages []language.Tag) string {
+	return TODO("implement CapnprotoRenderer.RenderWatchlist").(string)
 }
 
 func (CapnprotoRenderer) RenderExperiences(experiences datastructure.Experiences, languages []language.Tag) string {




diff --git a/front/html.go b/front/html.go
index 6a97673d243210f588454ba3391f2ee228c738bc..8ecadc229e582d56b28df4fc288246c8cb2a2857 100644
--- a/front/html.go
+++ b/front/html.go
@@ -5,7 +5,7 @@ 	"notabug.org/apiote/amuse/accounts"
 	"notabug.org/apiote/amuse/datastructure"
 	"notabug.org/apiote/amuse/i18n"
 	"notabug.org/apiote/amuse/tmdb"
-	"notabug.org/apiote/amuse/utils"
+	"notabug.org/apiote/amuse/config"
 	"notabug.org/apiote/amuse/wikidata"
 
 	"bytes"
@@ -59,8 +59,14 @@ 	user accounts.User
 }
 
 func render(languages []language.Tag, data RenderData, file string) string {
-	i18n.LoadServerLangs()
-	language := i18n.Match(languages)
+	err := i18n.LoadServerLangs()
+	if err != nil {
+		// todo return http:500
+	}
+	language, err := i18n.Match(languages)
+	if err != nil {
+		// todo return http:500
+	}
 	strings, err := i18n.LoadStrings(language)
 	if err != nil {
 		// todo return http:500
@@ -68,7 +74,7 @@ 	}
 
 	data.Strings = strings
 
-	t, _ := template.ParseFiles(utils.DataHome + "/templates/" + file + ".html")
+	t, _ := template.ParseFiles(config.DataHome + "/templates/" + file + ".html")
 	b := bytes.NewBuffer([]byte{})
 	err = t.Execute(b, data)
 	if err != nil {
@@ -111,7 +117,7 @@ 	data.State.User = r.user
 	return render(languages, data, "person")
 }
 
-func (r HtmlRenderer) RenderBook(book wikidata.Book, languages []language.Tag) string {
+func (r HtmlRenderer) RenderBook(book datastructure.Book, languages []language.Tag) string {
 	data := RenderData{Data: book}
 	data.State.User = r.user
 	return render(languages, data, "book")
@@ -154,7 +160,15 @@ 		Qr         template.URL
 	}{key.Secret(), sfaEnabled, username, template.URL(qr)}
 	data := RenderData{Data: secret}
 	data.State.Error = authError
-	return render(languages, data, "signup")
+
+	var template string
+	if config.OpenRegistration {
+		template = "signup"
+	} else {
+		template = "signup_locked"
+	}
+
+	return render(languages, data, template)
 }
 
 func (r HtmlRenderer) RenderSignedup(languages []language.Tag, recoveryCodes []string) string {
@@ -173,6 +187,12 @@ func (r HtmlRenderer) RenderTvQueue(tvqueue datastructure.TvQueue, languages []language.Tag) string {
 	data := RenderData{Data: tvqueue}
 	data.State.User = r.user
 	return render(languages, data, "tvqueue")
+}
+
+func (r HtmlRenderer) RenderReadlist(readlist datastructure.Readlist, languages []language.Tag) string {
+	data := RenderData{Data: readlist}
+	data.State.User = r.user
+	return render(languages, data, "readlist")
 }
 
 func (r HtmlRenderer) RenderExperiences(experiences datastructure.Experiences, languages []language.Tag) string {




diff --git a/front/renderer.go b/front/renderer.go
index df1c27130a500d7744ebdd5daa4a9b1161b7c02a..f5a6543927dc04326e6f2c437f9a3617706c3d6e 100644
--- a/front/renderer.go
+++ b/front/renderer.go
@@ -25,7 +25,7 @@ 	RenderSearch(*tmdb.SearchResults, *wikidata.SearchResults, []language.Tag) string
 	RenderIndex(string, []language.Tag) string
 	RenderTvSerie(*tmdb.TvSerie, []language.Tag) string
 	RenderPerson(*tmdb.Person, []language.Tag) string
-	RenderBook(wikidata.Book, []language.Tag) string
+	RenderBook(datastructure.Book, []language.Tag) string
 	RenderBookSerie(wikidata.BookSerie, []language.Tag) string
 	RenderAbout([]language.Tag) string
 	RenderErrorPage(int, []language.Tag) string
@@ -35,6 +35,7 @@ 	RenderSignup([]language.Tag, error, *otp.Key, bool, string, string) string
 	RenderSignedup([]language.Tag, []string) string
 	RenderWatchlist(datastructure.Watchlist, []language.Tag) string
 	RenderTvQueue(datastructure.TvQueue, []language.Tag) string
+	RenderReadlist(datastructure.Readlist, []language.Tag) string
 	RenderExperiences(datastructure.Experiences, []language.Tag) string
 }
 




diff --git a/go.mod b/go.mod
index b72aafa49b3fae1a42889bd08d3d0eded0001da6..a5a31662914dd55ad022efd2d6957dc05e68fc74 100644
--- a/go.mod
+++ b/go.mod
@@ -8,14 +8,12 @@ 	github.com/alecthomas/chroma v0.7.2 // indirect
 	github.com/bytesparadise/libasciidoc v0.4.0
 	github.com/chai2010/webp v1.1.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-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.5.0 // indirect
@@ -26,5 +24,4 @@ 	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
 )




diff --git a/go.sum b/go.sum
index 4011a704db57a6ac454fdb8116494b3507666825..ab8ad69ef45bb4c9c2545bbeb5838bada689ee77 100644
--- a/go.sum
+++ b/go.sum
@@ -1,17 +1,18 @@
 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 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
 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 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo=
 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 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY=
 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/chai2010/webp v1.1.0 h1:4Ei0/BRroMF9FaXDG2e4OxwFcuW2vcXd+A6tyqTJUQQ=
@@ -25,15 +26,11 @@ 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 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 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 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
 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=
@@ -41,20 +38,15 @@ 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 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
 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=
@@ -64,8 +56,6 @@ 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=
@@ -79,10 +69,9 @@ 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 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
 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=
@@ -100,8 +89,6 @@ 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=
@@ -111,8 +98,7 @@ 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 h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8=
 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=
@@ -133,21 +119,17 @@ 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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 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=
@@ -156,17 +138,7 @@ 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=




diff --git a/i18n/en-GB.toml b/i18n/en-GB.toml
index a1f788e304daf1e4d851b82047acf4c0ba2db411..a00330b30612f56ac18e93c672685b04ffa091ae 100644
--- a/i18n/en-GB.toml
+++ b/i18n/en-GB.toml
@@ -149,6 +149,7 @@ episodes = "Episodes"
 skipped = "Skipped"
 want_watch = "Want to watch"
 skip_specials = "Skip all specials"
+watched_whole_season = "Watched whole season"
 
 [person]
 cast = "Cast"
@@ -166,6 +167,10 @@ genre = "Genre"
 source = "Source"
 serie = "Series"
 part = "Part"
+want_read = "Want to read"
+read = "Read"
+Readlist = "Readlist"
+onReadlist = "You want to read this book"
 
 [bookSerie]
 author = "Author"
@@ -218,6 +223,7 @@ enable_sfa = "Enable second factor authentication"
 use_totp_app = "Use Your favourite TOTP app"
 confirm_sfa = "Confirm second factor authentication"
 sign_up = "Sign up"
+registration_closed = "Registrations are closed on this instance."
 already_have_account = "Already have an account?"
 log_in = "Log in"
 
@@ -249,9 +255,13 @@ mischief = "‘Mischief managed’"
 see_you = "See You next time…"
 
 [watchlist]
-title = "Watchilst — a·muse"
+title = "Watchlist — a·muse"
 filter = "filter watchlist"
 
+[readlist]
+title = "Readlist — a·muse"
+filter = "filter readlist"
+
 [tvqueue]
 title = "TV queue — a·muse"
 filter = "filter TV queue"
@@ -292,6 +302,11 @@ 422_character = "The Narrator"
 422_title = "Fight Club"
 #untranslatable
 422_name = "Unprocessable Entity"
+423_quote = "‘I can’t accept this.’"
+423_character = "Kristoff"
+423_title = "Frozen"
+#untranslatable
+423_name = "Locked"
 500_quote = "‘Houston, we may have a problem’"
 500_character = "Henry Brown"
 500_title = "Paddington"




diff --git a/i18n/i18n.go b/i18n/i18n.go
index 91a4b87dbd2f8dabfba142903506a83ac4b57eed..9c0247a81bc6601743d1bae9f063924d4399dd35 100644
--- a/i18n/i18n.go
+++ b/i18n/i18n.go
@@ -1,7 +1,7 @@
 package i18n
 
 import (
-	"notabug.org/apiote/amuse/utils"
+	"notabug.org/apiote/amuse/config"
 
 	"bytes"
 	"fmt"
@@ -17,6 +17,7 @@
 	"github.com/BurntSushi/toml"
 	"github.com/bytesparadise/libasciidoc"
 	"github.com/bytesparadise/libasciidoc/pkg/configuration"
+	"notabug.org/apiote/gott"
 )
 
 type Translation struct {
@@ -34,6 +35,7 @@ 	Signedup    map[string]string
 	Login       map[string]string
 	Loggedout   map[string]string
 	Watchlist   map[string]string
+	Readlist    map[string]string
 	Tvqueue     map[string]string
 	Experiences map[string]string
 	Error       map[string]string
@@ -41,17 +43,19 @@ }
 
 var serverLangs []language.Tag
 
-func LoadServerLangs() {
+func LoadServerLangs() error {
 	var tags []string
 	var defaultLocale string
 	defaultLocaleIndex := -1
-	filepath.Walk(utils.DataHome+"/i18n", func(path string, info os.FileInfo, err error) error {
+	err := filepath.Walk(config.DataHome+"/i18n", func(path string, info os.FileInfo, err error) error {
 		if !info.IsDir() {
 			if filepath.Ext(path) == ".toml" {
 				tag := strings.Replace(filepath.Base(path), ".toml", "", 1)
 				if tag == "default" {
-					// todo if is a link
-					p, _ := os.Readlink(path)
+					p, err := os.Readlink(path)
+					if err != nil {
+						return err
+					}
 					defaultLocale = strings.Replace(filepath.Base(p), ".toml", "", 1)
 				} else {
 					tags = append(tags, tag)
@@ -60,6 +64,9 @@ 			}
 		}
 		return nil
 	})
+	if err != nil {
+		return err
+	}
 
 	for i, tag := range tags {
 		if tag == defaultLocale {
@@ -71,21 +78,40 @@
 	for _, tag := range tags {
 		serverLangs = append(serverLangs, language.Make(tag))
 	}
+	return nil
 }
 
-func Match(acceptLanguages []language.Tag) string {
+func Match(acceptLanguages []language.Tag) (language.Tag, error) {
 	var matcher = language.NewMatcher(serverLangs)
 	tag, _, _ := matcher.Match(acceptLanguages...)
-	return tag.String()
+	b, s, r := tag.Raw()
+	t, err := language.Compose(b, s, r)
+	return t, err
+}
+
+func loadStringsFile(args ...interface{}) (interface{}, error) {
+	var (
+		strings Translation
+		err     error
+	)
+	if langTag, ok := args[0].(language.Tag); ok {
+		language := langTag.String()
+		_, err = toml.DecodeFile(filepath.Join(config.DataHome+"/i18n", language+".toml"), &strings)
+	}
+	args[1] = strings
+	return gott.Tuple(args), err
 }
 
-func LoadStrings(language string) (Translation, error) {
+func loadDefaultStringsFile(args ...interface{}) (interface{}, error) {
 	var strings Translation
-	var defaultTranslation Translation
-	_, err := toml.DecodeFile(filepath.Join(utils.DataHome+"/i18n", language+".toml"), &strings)
-	_, err2 := toml.DecodeFile(filepath.Join(utils.DataHome+"/i18n", "default.toml"), &defaultTranslation)
-	err = utils.Or(err, err2)
+	_, err := toml.DecodeFile(filepath.Join(config.DataHome+"/i18n", "default.toml"), &strings)
+	args[2] = strings
+	return gott.Tuple(args), err
+}
 
+func loadStrings(args ...interface{}) interface{} {
+	strings := args[1].(Translation)
+	defaultTranslation := args[2].(Translation)
 	stringsValue := reflect.ValueOf(&strings).Elem()
 	stringsType := stringsValue.Type()
 	for i := 0; i < stringsValue.NumField(); i++ {
@@ -100,8 +126,23 @@ 				stringsField.Interface().(map[string]string)[key] = entry
 			}
 		}
 	}
+	args[1] = strings
+	return gott.Tuple(args)
+}
 
-	return strings, err
+func LoadStrings(language language.Tag) (Translation, error) {
+	r, err := gott.
+		NewResult(gott.Tuple{language, Translation{}, Translation{}}).
+		Bind(loadStringsFile).
+		Bind(loadDefaultStringsFile).
+		Map(loadStrings).
+		Finish()
+
+	if err == nil {
+		return r.(gott.Tuple)[1].(Translation), nil
+	} else {
+		return Translation{}, err
+	}
 }
 
 func FormatDate(date time.Time, format string, translation map[string]string) string {
@@ -187,8 +228,13 @@ 	libasciidoc.ConvertToHTML(r, w, config)
 	output := bytes.ReplaceAll(w.Bytes(), []byte("\n"), []byte(""))
 
 	divRegex, err := regexp.Compile("<\\/?div[^>]*>")
-	pRegex, err2 := regexp.Compile("<\\/?p>")
-	err = utils.Or(err, err2)
+	if err != nil {
+		return template.HTML("<span style=\"color: red;\">error rendering asciidoc (div regex)</span>")
+	}
+	pRegex, err := regexp.Compile("<\\/?p>")
+	if err != nil {
+		return template.HTML("<span style=\"color: red;\">error rendering asciidoc (p regex)</span>")
+	}
 
 	output = divRegex.ReplaceAll(output, []byte(""))
 	output = pRegex.ReplaceAll(output, []byte(""))




diff --git a/i18n/pl-PL.toml b/i18n/pl-PL.toml
index 81e3224759b995a4777944a119e8b08252b8258b..acf01de75996567cfa3b6a5d7e2e9bf70472055d 100644
--- a/i18n/pl-PL.toml
+++ b/i18n/pl-PL.toml
@@ -206,6 +206,7 @@ enable_sfa = "Włącz dwuskładnikowe uwierzytelnianie"
 use_totp_app = "Użyj ulubionej aplikacji TOTP"
 confirm_sfa = "Potwierdź kod z aplikacji"
 sign_up = "Załóż konto"
+registration_closed = "Rejestracja jest zamknięta na tej instancji"
 already_have_account = "Masz już konto?"
 log_in = "Zaloguj się"
 
@@ -268,6 +269,9 @@ 410_title = "Piraci z Karaibów: Na krańcu świata"
 422_quote = "„Nie wiem. Nie rozumiem”"
 422_character = "Narrator"
 422_title = "Fight Club"
+423_quote = "„No nie mogę tego zaakceptować.”"
+423_character = "Kristoff"
+423_title = "Kraina lodu"
 500_quote = "„Houston, chyba mamy problem.”"
 500_character = "Henry Brown"
 500_title = "Paddington"




diff --git a/inventaire/book.go b/inventaire/book.go
new file mode 100644
index 0000000000000000000000000000000000000000..ca0f56cb3c9fcac14e0d7c48b5fd2e4eb7a6a6e6
--- /dev/null
+++ b/inventaire/book.go
@@ -0,0 +1,206 @@
+package inventaire
+
+import (
+	"notabug.org/apiote/amuse/datastructure"
+	"notabug.org/apiote/amuse/network"
+
+	"database/sql"
+	"encoding/json"
+	"net/http"
+	"strings"
+
+	"notabug.org/apiote/gott"
+)
+
+type InventaireUriResponse struct {
+	Entities map[string]struct {
+		Labels       map[string]string
+		Claims       map[string][]string
+		Descriptions map[string]string
+		Image        struct {
+			Url     string
+			Credits struct {
+				Text string
+				Url  string
+			}
+		}
+		Sitelinks map[string]string
+	}
+}
+
+type InventaireClaimResponse struct {
+	Uris []string
+}
+
+func createBookRequest(args ...interface{}) (interface{}, error) {
+	request := args[0].(*network.Request)
+	result := args[1].(*network.Result)
+	result.Client = &http.Client{}
+	httpRequest, err := http.NewRequest("GET", "https://inventaire.io/api/entities?action=by-uris&uris="+request.Id+"&refresh=false", nil)
+	result.Request = httpRequest
+	return gott.Tuple(args), err
+}
+
+func findByTag(haystack map[string]string, tag, suffix string) string {
+	tag = strings.ToLower(tag)
+	result := haystack[tag+suffix]
+	if result == "" {
+		tag = strings.Split(tag, "-")[0]
+		result = haystack[tag+suffix]
+	}
+	if result == "" {
+		tag = "en"
+		result = haystack[tag+suffix]
+	}
+	return result
+}
+
+func unmarshalBook(args ...interface{}) (interface{}, error) {
+	id := args[0].(*network.Request).Id
+	language := args[0].(*network.Request).Language
+	result := args[1].(*network.Result)
+	response := &InventaireUriResponse{}
+	err := json.Unmarshal(result.Body, response)
+	book := &datastructure.Book{}
+	book.Id = id
+	book.Uri = "/books/" + id
+	source := datastructure.Source{
+		Url:  "https://inventaire.io/entity/" + id,
+		Name: "Inventaire",
+	}
+	book.Source = append(book.Source, source)
+	book.Authors = append(book.Authors, response.Entities[id].Claims["wdt:P50"]...)
+	book.Title = findByTag(response.Entities[id].Labels, language, "")
+	book.Article = findByTag(response.Entities[id].Sitelinks, language, "wiki")
+	book.Description = findByTag(response.Entities[id].Descriptions, language, "")
+	result.Result = book
+	return gott.Tuple(args), err
+}
+
+func getBook(args ...interface{}) (interface{}, error) {
+	id := args[0].(string)
+	language := args[1].(string)
+	book := args[2].(*datastructure.Book)
+	result, err := gott.
+		NewResult(gott.Tuple{&network.Request{Id: id, Language: language}, &network.Result{}}).
+		Bind(createBookRequest).
+		//Bind(cache.getCacheEntry).
+		Map(network.AddHeaders).
+		Bind(network.DoRequest).
+		Bind(network.HandleRequestError).
+		Bind(network.ReadResponse).
+		//Tee(cache.cleanCache).
+		//Tee(cache.saveCacheEntry).
+		Bind(unmarshalBook).
+		Finish()
+
+	if err == nil {
+		*book = *result.(gott.Tuple)[1].(*network.Result).Result.(*datastructure.Book)
+	}
+	return gott.Tuple(args), err
+}
+
+func createAuthorsRequest(args ...interface{}) (interface{}, error) {
+	request := args[0].(*network.Request)
+	result := args[1].(*network.Result)
+	result.Client = &http.Client{}
+	httpRequest, err := http.NewRequest("GET", "https://inventaire.io/api/entities?action=by-uris&uris="+request.Id+"&refresh=false", nil)
+	result.Request = httpRequest
+	return gott.Tuple(args), err
+}
+
+func unmarshalAuthors(args ...interface{}) (interface{}, error) {
+	id := args[0].(*network.Request).Id
+	language := args[0].(*network.Request).Language
+	result := args[1].(*network.Result)
+	response := &InventaireUriResponse{}
+	err := json.Unmarshal(result.Body, response)
+	ids := strings.Split(id, "|")
+	result.Result = []string{}
+	for _, authorId := range ids {
+		result.Result = append(result.Result.([]string), findByTag(response.Entities[authorId].Labels, language, ""))
+	}
+	return gott.Tuple(args), err
+}
+
+func getAuthors(args ...interface{}) (interface{}, error) {
+	language := args[1].(string)
+	book := args[2].(*datastructure.Book)
+	id := strings.Join(book.Authors, "|")
+	author, err := gott.
+		NewResult(gott.Tuple{&network.Request{Id: id, Language: language}, &network.Result{}}).
+		Bind(createAuthorsRequest).
+		//Bind(cache.getCacheEntry).
+		Map(network.AddHeaders).
+		Bind(network.DoRequest).
+		Bind(network.HandleRequestError).
+		Bind(network.ReadResponse).
+		//Tee(cache.cleanCache).
+		//Tee(cache.saveCacheEntry).
+		Bind(unmarshalAuthors).
+		Finish()
+
+	if err == nil {
+		book.Authors = author.(gott.Tuple)[1].(*network.Result).Result.([]string)
+	}
+	return gott.Tuple(args), err
+}
+
+func createEditionRequest(args ...interface{}) (interface{}, error) {
+	request := args[0].(*network.Request)
+	result := args[1].(*network.Result)
+	result.Client = &http.Client{}
+	httpRequest, err := http.NewRequest("GET", "https://inventaire.io/api/entities?action=reverse-claims&value="+request.Id+"&property=wdt:P629&refresh=false", nil)
+	result.Request = httpRequest
+	return gott.Tuple(args), err
+}
+
+func unmarshalEdition(args ...interface{}) (interface{}, error) {
+	result := args[1].(*network.Result)
+	response := &InventaireClaimResponse{}
+	err := json.Unmarshal(result.Body, response)
+	if len(response.Uris) > 0 {
+		result.Result = response.Uris[0]
+	} else {
+		result.Result = ""
+	}
+	return gott.Tuple(args), err
+}
+
+func getEditions(args ...interface{}) (interface{}, error) {
+	id := args[0].(string)
+	language := args[1].(string)
+	book := args[2].(*datastructure.Book)
+	edition, err := gott.
+		NewResult(gott.Tuple{&network.Request{Id: id, Language: language}, &network.Result{}}).
+		Bind(createEditionRequest).
+		//Bind(cache.getCacheEntry).
+		Map(network.AddHeaders).
+		Bind(network.DoRequest).
+		Bind(network.HandleRequestError).
+		Bind(network.ReadResponse).
+		//Tee(cache.cleanCache).
+		//Tee(cache.saveCacheEntry).
+		Bind(unmarshalEdition).
+		Finish()
+
+	if err == nil {
+		book.Cover = edition.(gott.Tuple)[1].(*network.Result).Result.(string)
+	}
+	return gott.Tuple(args), err
+}
+
+func GetBook(id string, language string, connection *sql.DB) (*datastructure.Book, error) {
+	r, err := gott.
+		NewResult(gott.Tuple{id, language, &datastructure.Book{}}).
+		Bind(getBook).
+		Bind(getAuthors).
+		Bind(getEditions).
+		Finish()
+
+	if err != nil {
+		return &datastructure.Book{}, err
+	} else {
+		return r.(gott.Tuple)[2].(*datastructure.Book), nil
+	}
+}




diff --git a/libamuse/account.go b/libamuse/account.go
index 9f8ea3ac0c2575b28b4607c0da682bab1e8ffcbb..0827924d75a7b20fbb100bb6ceb3cc7ce55fb7d9 100644
--- a/libamuse/account.go
+++ b/libamuse/account.go
@@ -60,7 +60,8 @@ 	return gott.Tuple(args), err
 }
 
 func getItem(args ...interface{}) (interface{}, error) {
-	itemType := args[2].(string)
+	itemTypeName := args[2].(string)
+	itemType := datastructure.ItemType(itemTypeName)
 	var arg interface{}
 	var err error
 	switch itemType {
@@ -81,8 +82,18 @@ 			Finish()
 		if err == nil {
 			args = arg.(gott.Tuple)
 		}
+	case datastructure.ItemTypeBook:
+		arg, err = gott.
+			NewResult(gott.Tuple(args)).
+			Bind(getBook).
+			Bind(getDescription).
+			Bind(getCover).
+			Finish()
+		if err == nil {
+			args = arg.(gott.Tuple)
+		}
 	default:
-		err = errors.New("Wrong ItemType: " + itemType)
+		err = errors.New("Wrong ItemType: " + itemTypeName)
 	}
 	return gott.Tuple(args), err
 }
@@ -152,7 +163,7 @@ 	id := strings.Split(itemId, "/")
 	if len(id) > 1 && id[1][3] == 'A' {
 		arg, err := gott.
 			NewResult(gott.Tuple(args)).
-			Bind(getSeason0).
+			Bind(getSeason).
 			Finish()
 		if err == nil {
 			args = arg.(gott.Tuple)
@@ -175,11 +186,13 @@
 	id := strings.Split(itemId, "/")
 	if len(id) > 1 && id[1][3] == 'A' {
 		serie := result.result.(*tmdb.TvSerie)
+		var season int
+		fmt.Sscanf(id[1][1:3], "%d", &season)
 		episodes := []string{}
-		for _, episode := range serie.Seasons[0].Episodes {
+		for _, episode := range serie.Seasons[season].Episodes {
 			episodes = append(episodes, data.id+"/"+episode.Episode_code)
 		}
-		refs, err = db.SkipSpecials(result.user.Username, id[0], episodes, datastructure.ItemType(itemType))
+		refs, err = db.WatchWholeSerie(result.user.Username, id[0], episodes, datastructure.ItemType(itemType), t)
 	} else {
 		refs, err = db.AddToExperiences(result.user.Username, itemId, datastructure.ItemType(itemType), t)
 	}
@@ -202,11 +215,15 @@
 	var err error
 	if len(id) > 1 && id[1][3] == 'A' {
 		serie := result.result.(*tmdb.TvSerie)
-		episodes := []string{}
-		for _, episode := range serie.Seasons[0].Episodes {
-			episodes = append(episodes, data.id+"/"+episode.Episode_code)
+		var season int
+		fmt.Sscanf(id[1][1:3], "%d", &season)
+		if season == 0 {
+			episodes := []string{}
+			for _, episode := range serie.Seasons[0].Episodes {
+				episodes = append(episodes, data.id+"/"+episode.Episode_code)
+			}
+			err = db.ClearSpecials(result.user.Username, id[0], episodes, datastructure.ItemType(itemType))
 		}
-		err = db.ClearSpecials(result.user.Username, id[0], episodes, datastructure.ItemType(itemType))
 	}
 	return gott.Tuple(args), err
 }




diff --git a/libamuse/book.go b/libamuse/book.go
index 7d19e434bba573f5b1650f0f20fc5384a4bd0d10..e8038972a5b2c594f2481faad47e746e2a08371a 100644
--- a/libamuse/book.go
+++ b/libamuse/book.go
@@ -1,8 +1,13 @@
 package libamuse
 
 import (
+	"notabug.org/apiote/amuse/accounts"
+	"notabug.org/apiote/amuse/datastructure"
+	"notabug.org/apiote/amuse/inventaire"
 	"notabug.org/apiote/amuse/wikidata"
-	"notabug.org/apiote/amuse/accounts"
+	"notabug.org/apiote/amuse/db"
+
+	"errors"
 
 	"notabug.org/apiote/gott"
 )
@@ -11,7 +16,17 @@ func getBook(args ...interface{}) (interface{}, error) {
 	data := args[0].(*RequestData)
 	result := args[1].(*Result)
 	languages := result.languages
-	book, err := wikidata.GetBook(data.id, languages[0].String(), data.connection)
+	var (
+		book *datastructure.Book
+		err  error
+	)
+	if data.id[:2] == "wd" {
+		book, err = wikidata.GetBook(data.id, languages[0].String(), data.connection)
+	} else if data.id[:3] == "inv" {
+		book, err = inventaire.GetBook(data.id, languages[0].String(), data.connection)
+	} else {
+		err = errors.New("Wrong scheme")
+	}
 	result.result = book
 	return gott.Tuple(args), err
 }
@@ -19,16 +34,30 @@
 func getCover(args ...interface{}) (interface{}, error) {
 	data := args[0].(*RequestData)
 	result := args[1].(*Result)
-	book := result.result.(*wikidata.Book)
+	book := result.result.(*datastructure.Book)
 	languages := result.languages
 	cover, err := wikidata.GetCover(data.id, languages[0].String(), data.connection)
 	book.Cover = cover
 	return gott.Tuple(args), err
 }
 
+func getBookExperiences(args ...interface{}) (interface{}, error) {
+	data := args[0].(*RequestData)
+	result := args[1].(*Result)
+	book := result.result.(*datastructure.Book)
+
+	if result.user.IsEmpty() {
+		return gott.Tuple(args), nil
+	}
+
+	exp, err := db.GetItemExperiences(result.user.Username, data.id, datastructure.ItemTypeBook)
+	book.Experiences = exp[data.id]
+	return gott.Tuple(args), err
+}
+
 func renderBook(args ...interface{}) interface{} {
 	result := args[1].(*Result)
-	book := result.result.(*wikidata.Book)
+	book := result.result.(*datastructure.Book)
 	result.page = result.renderer.RenderBook(*book, result.languages)
 	return gott.Tuple(args)
 }
@@ -42,6 +71,8 @@ 		Bind(verifyToken).
 		Bind(getBook).
 		Bind(getDescription).
 		Bind(getCover).
+		Bind(getBookExperiences).
+		Bind(isOnWantList).
 		Bind(createRenderer).
 		Map(renderBook).
 		Finish()




diff --git a/libamuse/bookserie.go b/libamuse/bookserie.go
index 361177a772cb035150a1a3b1bcae977b196c89eb..ca65bc94686eba0c4c1066142ac75ad7435609c4 100644
--- a/libamuse/bookserie.go
+++ b/libamuse/bookserie.go
@@ -2,7 +2,6 @@ package libamuse
 
 import (
 	"notabug.org/apiote/amuse/wikidata"
-	"notabug.org/apiote/amuse/utils"
 	"notabug.org/apiote/amuse/accounts"
 
 	"strings"
@@ -33,15 +32,15 @@ 	result := args[1].(*Result)
 	bookSerie := result.result.(*wikidata.BookSerie)
 	languages := result.languages
 
-	var err error
-	
 	for i, part := range bookSerie.SortedParts {
 		partId := strings.Replace(part.Uri, "/books/", "", 1)
-		cover, e := wikidata.GetCover(partId, languages[0].String(), data.connection)
-		err = utils.Or(err, e)
+		cover, err := wikidata.GetCover(partId, languages[0].String(), data.connection)
+		if err != nil {
+			return gott.Tuple(args), err
+		}
 		bookSerie.SortedParts[i].Cover = cover
 	}
-	return gott.Tuple(args), err
+	return gott.Tuple(args), nil
 }
 
 func renderBookSerie(args ...interface{}) interface{} {




diff --git a/libamuse/common.go b/libamuse/common.go
index 8605e85dc1ff7fa0764b9f715babab9aa01e994e..b7bc494eb58b1fd8eabb24c0034756e29ea0924e 100644
--- a/libamuse/common.go
+++ b/libamuse/common.go
@@ -2,11 +2,11 @@ package libamuse
 
 import (
 	"notabug.org/apiote/amuse/accounts"
+	"notabug.org/apiote/amuse/datastructure"
+	"notabug.org/apiote/amuse/db"
 	"notabug.org/apiote/amuse/front"
 	"notabug.org/apiote/amuse/tmdb"
 	"notabug.org/apiote/amuse/wikidata"
-	"notabug.org/apiote/amuse/datastructure"
-	"notabug.org/apiote/amuse/db"
 
 	"database/sql"
 	"errors"
@@ -77,9 +77,15 @@
 func getDescription(args ...interface{}) (interface{}, error) {
 	data := args[0].(*RequestData)
 	result := args[1].(*Result)
-	work := result.result.(wikidata.Work)
-	languages := result.languages
-	description, err := wikidata.GetWorkDescription(work.GetArticle(), languages[0].String(), data.connection)
+	work := result.result.(datastructure.Work)
+	var (
+		description string
+		err         error
+	)
+	if work.GetArticle() != "" {
+		languages := result.languages
+		description, err = wikidata.GetWorkDescription(work.GetArticle(), languages[0].String(), data.connection)
+	}
 	work.SetDescription(description)
 	return gott.Tuple(args), err
 }
@@ -100,23 +106,23 @@ 	list := result.result.(datastructure.List)
 	genres, err := tmdb.GetGenres(result.languages[0].String(), list.GetType())
 	list.SetGenres(genres)
 	result.result = list
-	
+
 	return gott.Tuple(args), err
 }
 
 func isOnWantList(args ...interface{}) (interface{}, error) {
 	data := args[0].(*RequestData)
 	result := args[1].(*Result)
-	show := result.result.(tmdb.Show)
+	item := result.result.(datastructure.Item)
 
 	if result.user.IsEmpty() {
 		return gott.Tuple(args), nil
 	}
 
-	itemType := tmdb.GetItemTypeFromShow(show)
+	itemType := item.GetItemType()
 
 	isOnList, err := db.IsOnWantList(result.user.Username, data.id, itemType)
-	show.SetOnWantList(isOnList)
+	item.SetOnWantList(isOnList)
 	return gott.Tuple(args), err
 }
 
@@ -131,7 +137,6 @@
 	err := db.UpdateCacheItem(item.GetItemType(), data.id, itemInfo)
 	return gott.Tuple(args), err
 }
-
 
 type RequestData struct {
 	id         string




diff --git a/libamuse/manage.go b/libamuse/manage.go
index 7fa56f2d726ebdb52692c543e040d01ce67fd8c9..f93bed1ab5cc7b06014e5024a6c4aaca84e1ef79 100644
--- a/libamuse/manage.go
+++ b/libamuse/manage.go
@@ -1,9 +1,38 @@
 package libamuse
 
 import (
+	"notabug.org/apiote/amuse/accounts"
+	"notabug.org/apiote/amuse/datastructure"
 	"notabug.org/apiote/amuse/db"
+
+	"strings"
+
+	"fmt"
 )
 
 func MakeAdmin(username string) error {
 	return db.MakeAdmin(username)
 }
+
+func TouchWantlist() error {
+	uris, err := db.GetWantlistUris()
+	if err != nil {
+		return err
+	}
+	auth := accounts.Authentication{Token: ""}
+	for _, uri := range uris {
+		fmt.Printf("Touching %s\n", uri)
+		type_id := strings.Split(uri, "/")
+		itemType := datastructure.ItemType(type_id[0])
+		itemId := type_id[1]
+		switch itemType {
+		case datastructure.ItemTypeFilm:
+			_, _ = ShowFilm(itemId, "en-GB", "text/html", auth)
+		case datastructure.ItemTypeTvserie:
+			_, _ = ShowTvSerie(itemId, "", "en-GB", "text/html", auth)
+		}
+
+	}
+
+	return nil
+}




diff --git a/libamuse/readlist.go b/libamuse/readlist.go
new file mode 100644
index 0000000000000000000000000000000000000000..646240caf1f084b984ba3e89e2d769969e5cfc1e
--- /dev/null
+++ b/libamuse/readlist.go
@@ -0,0 +1,54 @@
+package libamuse
+
+import (
+	"notabug.org/apiote/amuse/accounts"
+	"notabug.org/apiote/amuse/db"
+	"notabug.org/apiote/amuse/datastructure"
+
+	"notabug.org/apiote/gott"
+)
+
+func getReadlist(args ...interface{}) (interface{}, error) {
+	request := args[0].(*RequestData)
+	result := args[1].(*Result)
+	page := args[2].(int)
+	watchlist, err := db.GetReadlist(result.user.Username, request.id, page)
+	result.result = &watchlist
+
+	return gott.Tuple(args), err
+}
+
+func renderReadlist(args ...interface{}) interface{} {
+	request := args[0].(*RequestData)
+	result := args[1].(*Result)
+	page := args[2].(int)
+	readlist := result.result.(*datastructure.Readlist)
+	readlist.Page = page
+	readlist.Query = request.id
+	result.page = result.renderer.RenderReadlist(*readlist, result.languages)
+
+	return gott.Tuple(args)
+}
+
+func ShowReadlist(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(getReadlist).
+		Bind(createRenderer).
+		Map(renderReadlist).
+		Finish()
+
+	if err != nil {
+		return "", err
+	} else {
+		return r.(gott.Tuple)[1].(*Result).page, nil
+	}
+}




diff --git a/libamuse/serie.go b/libamuse/serie.go
index 521d830267da953390b8db839e9c2667c9cac83e..15c27c6a605df3f2d004f0dfa3f7a3bdd7b17f41 100644
--- a/libamuse/serie.go
+++ b/libamuse/serie.go
@@ -2,11 +2,14 @@ package libamuse
 
 import (
 	"notabug.org/apiote/amuse/accounts"
+	"notabug.org/apiote/amuse/datastructure"
 	"notabug.org/apiote/amuse/db"
 	"notabug.org/apiote/amuse/tmdb"
-	"notabug.org/apiote/amuse/datastructure"
 
+	"fmt"
 	"sort"
+	"strings"
+	"time"
 
 	"notabug.org/apiote/gott"
 )
@@ -37,12 +40,17 @@ 	tvSerie.Seasons = seasons
 	return gott.Tuple(args), err
 }
 
-func getSeason0(args ...interface{}) (interface{}, error) {
+func getSeason(args ...interface{}) (interface{}, error) {
 	result := args[1].(*Result)
+	itemId := args[3].(string)
+	id := strings.Split(itemId, "/")
+	var seasonNumber int
+	fmt.Sscanf(id[1][1:3], "%d", &seasonNumber)
 	tvSerie := result.result.(*tmdb.TvSerie)
 	languages := result.languages
-	seasons, err := tmdb.GetSeason0(tvSerie, languages[0].String())
-	tvSerie.Seasons = seasons
+	season, err := tmdb.GetSeason(tvSerie, languages[0].String(), seasonNumber)
+	tvSerie.Seasons = make([]tmdb.Season, seasonNumber+1)
+	tvSerie.Seasons[seasonNumber] = season
 	return gott.Tuple(args), err
 }
 
@@ -134,7 +142,7 @@ 		}
 		watchedAndSkipped += 1
 	}
 	if tvSerie.AllEpisodes > 0 {
-		tvSerie.Progress = min(watched * 100 / (tvSerie.AllEpisodes - skipped), 100)
+		tvSerie.Progress = min(watched*100/(tvSerie.AllEpisodes-skipped), 100)
 	}
 	tvSerie.WatchedEpisodes = watched
 	tvSerie.SkippedEpisodes = skipped
@@ -162,6 +170,47 @@
 	return gott.Tuple(args), err
 }
 
+func setSpoilers(args ...interface{}) interface{} {
+	result := args[1].(*Result)
+	tvSerie := result.result.(*tmdb.TvSerie)
+	nextEpisode := tvSerie.Next_episode_to_watch
+	nextCode := nextEpisode.Episode_code
+	if nextCode == "" {
+		if tvSerie.WatchedEpisodes > 0 {
+			nextCode = "S99E99"
+		} else {
+			nextCode = "S01E01"
+		}
+	}
+	nextDate := nextEpisode.Air_date
+	if nextDate.IsZero() {
+		if tvSerie.WatchedEpisodes > 0 {
+			nextDate = time.Now()
+		} else {
+			for _, season := range tvSerie.Seasons {
+				if season.Season_number == 1 && len(season.Episodes) > 0 {
+					nextDate = season.Episodes[0].Air_date
+				}
+			}
+		}
+	}
+	for s := 0; s < len(tvSerie.Seasons); s++ {
+		for e := 0; e < len(tvSerie.Seasons[s].Episodes); e++ {
+			episode := &(tvSerie.Seasons[s].Episodes[e])
+			if episode.Air_date.After(nextDate) ||
+				(episode.Air_date == nextDate && episode.Episode_code > nextCode) {
+				episode.ContainsSpoilers = true
+			}
+		}
+	}
+	episode := &(tvSerie.Last_episode_to_air)
+	if episode.Air_date.After(nextDate) ||
+		(episode.Air_date == nextDate && episode.Episode_code > nextCode) {
+		episode.ContainsSpoilers = true
+	}
+	return gott.Tuple(args)
+}
+
 func getEpisodesExperiences(args ...interface{}) (interface{}, error) {
 	result := args[1].(*Result)
 	tvSerie := result.result.(*tmdb.TvSerie)
@@ -197,6 +246,7 @@ 		Map(countAllEpisodes).
 		Bind(calculateProgress).
 		Bind(findNextEpisode).
 		Bind(getEpisodesExperiences).
+		Map(setSpoilers).
 		Bind(createRenderer).
 		Map(renderSerie).
 		Finish()




diff --git a/libamuse/user.go b/libamuse/user.go
index 3c26306818b7c84cfae7179c873a43b9b234ee8a..2cdfeee34c784d522a169312b7b74cbf8ef8f60a 100644
--- a/libamuse/user.go
+++ b/libamuse/user.go
@@ -2,13 +2,13 @@ package libamuse
 
 import (
 	"notabug.org/apiote/amuse/accounts"
+	"notabug.org/apiote/amuse/config"
 	"notabug.org/apiote/amuse/db"
-	"notabug.org/apiote/amuse/network"
 
 	"crypto/sha256"
 	"encoding/base64"
 	"errors"
-	"net/http"
+	"io/ioutil"
 
 	"notabug.org/apiote/gott"
 )
@@ -52,7 +52,7 @@ func checkEtag(args ...interface{}) (interface{}, error) {
 	request := args[0].(*RequestData)
 	result := args[1].(*Result)
 	h := sha256.New()
-	_, err := h.Write([]byte(result.page))
+	_, err := h.Write(result.result.([]byte))
 	if err != nil {
 		return gott.Tuple(args), err
 	}
@@ -64,26 +64,6 @@ 	result.page = etag
 	return gott.Tuple(args), nil
 }
 
-func createPlaceholderRequest(args ...interface{}) (interface{}, error) {
-	request := args[0].(*network.Request)
-	result := args[1].(*network.Result)
-	small := args[2].(bool)
-	result.Client = &http.Client{}
-	size := "512"
-	if small {
-		size = "40"
-	}
-	httpRequest, err := http.NewRequest("GET", "https://api.adorable.io/avatars/"+size+"/"+request.Id+".png", nil)
-	result.Request = httpRequest
-	return gott.Tuple(args), err
-}
-
-func unmarshalPlaceholder(args ...interface{}) interface{} {
-	result := args[1].(*network.Result)
-	result.Result = result.Body
-	return gott.Tuple(args)
-}
-
 func recovery(args ...interface{}) (interface{}, error) {
 	err := args[3].(error)
 	switch err.Error() {
@@ -95,28 +75,11 @@ 	}
 }
 
 func getPlaceholder(args ...interface{}) (interface{}, error) {
-	request := args[0].(*RequestData)
 	result := args[1].(*Result)
-	small := args[2].(bool)
-	r, err := gott.
-		NewResult(gott.Tuple{&network.Request{Id: request.id, Etag: request.etag}, &network.Result{}, small}).
-		Bind(createPlaceholderRequest).
-		Map(network.AddHeaders).
-		Bind(network.DoRequest).
-		Bind(network.HandleRequestError).
-		Bind(network.ReadResponse).
-		Map(unmarshalPlaceholder).
-		// todo get etag
-		Finish()
-
-	if err != nil {
-		return gott.Tuple(args), err
-	}
-
-	result.result = r.(gott.Tuple)[1].(*network.Result).Result.([]byte)
-	result.result2 = "image/png"
-
-	return gott.Tuple(args), nil
+	img, err := ioutil.ReadFile(config.DataHome + "/static/img/avatar.webp") // todo path
+	result.result = img
+	result.result2 = "image/webp"
+	return gott.Tuple(args), err
 }
 
 func ShowUserAvatar(username, etagReq string, auth accounts.Authentication, small bool) (Avatar, error) {
@@ -129,8 +92,8 @@ 		Bind(verifyToken).
 		Bind(verifyUser).
 		Bind(getUser).
 		Bind(getAvatar).
-		Bind(checkEtag).
 		Recover(recovery).
+		Bind(checkEtag).
 		Finish()
 
 	if err != nil {




diff --git a/main.go b/main.go
index fbbf1b767e1eaaa9d7ab4bd3aa4ecc416810fcf6..629a6973e230e1a538ed1d2a6c7f869c500d1a41 100644
--- a/main.go
+++ b/main.go
@@ -1,22 +1,36 @@
 package main
 
 import (
-	"notabug.org/apiote/amuse/libamuse"
-	"notabug.org/apiote/amuse/utils"
+	"notabug.org/apiote/amuse/config"
 	"notabug.org/apiote/amuse/db"
+	"notabug.org/apiote/amuse/libamuse"
 
 	"flag"
 	"fmt"
 	"os"
+	"log"
 )
 
 func main() {
-	port := flag.Int("p", 5008, "port to run amuse on")
-	dataHome := flag.String("d", "/usr/local/share/amuse", "data directory")
+	port := flag.Int("p", -1, "port to run amuse on")
+	address := flag.String("a", "", "address to run amuse on")
+	dataHome := flag.String("d", "", "data directory")
 	manage := flag.String("m", "", "manage command")
+	configPath := flag.String("c", "/etc/amuse.toml", "configPath")
 	flag.Parse()
 
-	utils.DataHome = *dataHome
+	config.ReadConfig(*configPath)
+	if *dataHome != "" {
+		config.DataHome = *dataHome
+	}
+	if *port > 0 {
+		config.Port = uint(*port)
+	}
+	if *address != "" {
+		config.Address = *address
+	}
+
+	validateState()
 
 	db.Migrate()
 
@@ -31,7 +45,24 @@ 			os.Exit(1)
 		} else {
 			return
 		}
+	case "touch":
+		err := libamuse.TouchWantlist()
+		if err != nil {
+			os.Exit(1)
+		} else {
+			return
+		}
 	}
 
-	route(*port)
+	route(config.Port)
+}
+
+func validateState() {
+	_, err := os.Readlink(config.DataHome + "/i18n/default.toml")
+	if err != nil {
+		log.Println("WARN: i18n/default.toml is not a symbolic link; translations fallback will not work")
+	}
+	if config.TmdbApiKey == "" {
+		log.Fatalln("ERR: TmdbApiKey not specified in config")
+	}
 }




diff --git a/mkfile b/mkfile
index e4a9d8db74dfa544e2c6e6f178acf076c5ea6931..196c2318259a0bd75ebdfd5a2763bb17dab9f3f6 100644
--- a/mkfile
+++ b/mkfile
@@ -3,7 +3,7 @@
 all:V: $ALL
 reallyall:V: $ALL pymodule
 
-amuse: main.go router.go go.mod go.sum `echo front/*.go i18n/*.go libamuse/*.go protocol/*.go tmdb/*.go utils/*.go wikidata/*.go db/*.go datastructure/*.go network/*.go accounts/*.go`
+amuse: main.go router.go go.mod go.sum `echo front/*.go i18n/*.go libamuse/*.go tmdb/*.go wikidata/*.go inventaire/*.go db/*.go datastructure/*.go network/*.go accounts/*.go`
 	go build -ldflags "-s -w -linkmode external -extldflags -static"
 static/img/%.webp: static/img/%.svg
 	rendersvg static/img/$stem.svg static/img/$stem.png
@@ -31,11 +31,6 @@ uninstall:V:
 	rm $PREFIX/bin/amuse
 	rm -r $PREFIX/share/amuse
 
-# https://github.com/go-python/gopy
-pymodule:QV: `echo **/*.go`
-	echo "NOTE: This only works with glibc"
-	gopy build -output pymodule notabug.org/apiote/amuse/libamuse
-	cd pymodule
-	go build -ldflags "-s -w" -buildmode=c-archive -o libamuse_go.a .
-	gcc libamuse.c libamuse_go.a -o _libamuse.so $(python3.7-config --cflags) $(python3.7-config --ldflags) -fPIC --shared
-	rm -f Makefile __pycache__ build.py libamuse.c libamuse_go.h __init__.py libamuse.go libamuse_go.so libamuse_go.a
+zip:V: amuse.tgz
+amuse.tgz: amuse `echo templates/* static/* i18n/*`
+	tar czf amuse.tgz templates i18n/??-??.toml static amuse




diff --git a/network/common.go b/network/common.go
index f0bd39c858476fa55d43dcbf407f24b7c0a7d9f1..0e6318b196e0e9950c924d2dac0a2fb55cb8f36e 100644
--- a/network/common.go
+++ b/network/common.go
@@ -44,7 +44,7 @@
 func DoRequest(args ...interface{}) (interface{}, error) {
 	result := args[1].(*Result)
 	resp, err := result.Client.Do(result.Request)
-	if resp.StatusCode != 304 {
+	if err == nil && resp.StatusCode != 304 {
 		result.Response = resp
 	}
 	return gott.Tuple(args), err




diff --git a/protocol/amuse.proto b/protocol/amuse.proto
deleted file mode 100644
index 0a9336e08ac3c5fbdcc60ffff92cc4748713259a..0000000000000000000000000000000000000000
--- a/protocol/amuse.proto
+++ /dev/null
@@ -1,73 +0,0 @@
-using Go = import "/go.capnp";
-@0xcd7129bab3129d29;
-$Go.package("protocol");
-$Go.import("protocol");
-
-struct Film {
-	id @0 :UInt64;
-	title @1 :Text;
-	genres @2 :List(Text);
-	releaseDate @3 :Date;
-
-	struct Date {
-		year @0 :Int16;
-		month @1 :UInt8;
-		day @2 :UInt8;
-	}
-
-	originalTitle @4 :Text;
-	overview @5 :Text;
-	tagline @6 :Text;
-	runtime @7 :UInt16;
-	isAdult @8 :Bool;
-	mark @9 :Float32;
-	voteCount @10 :UInt64;
-
-	status @11 :Status;
-
-	enum Status {
-		rumored @0         $Go.tag("Rumored");
-		planned @1         $Go.tag("Planned");
-		inProduction @2    $Go.tag("In Production");
-		postProduction @3  $Go.tag("Post Production");
-		released @4        $Go.tag("Released");
-		cancelled @5       $Go.tag("Cancelled");
-	}
-
-	posterPath @12 :Text;
-	backdropPath @13 :Text;
-	
-	collection @14 :Collection;
-
-	struct Collection {
-		name @0 :Text;
-		parts @1 :List(CollectionPart);
-
-		struct CollectionPart {
-			id @0 :UInt64;
-			title @1 :Text;
-			releaseDate @2 :Date;
-			posterPath @3 :Text;
-		}
-	}
-
-	cast @15 :List(Character);
-
-	struct Character {
-		person @0 :Person;
-		name @1 :Text;
-	}
-
-	crew @16: List(CrewMember);
-
-	struct CrewMember {
-		person @0 :Person;
-		job @1 :Text;
-	}
-}
-
-struct Person {
-	id @0 :UInt64;
-	name @1 :Text;
-	photoPath @2 :Text;
-}




diff --git a/protocol/amuse.proto.go b/protocol/amuse.proto.go
deleted file mode 100644
index d65d28d24e62a43d79c4ab7b4158ed690babad55..0000000000000000000000000000000000000000
--- a/protocol/amuse.proto.go
+++ /dev/null
@@ -1,1126 +0,0 @@
-// Code generated by capnpc-go. DO NOT EDIT.
-
-package protocol
-
-import (
-	math "math"
-	capnp "zombiezen.com/go/capnproto2"
-	text "zombiezen.com/go/capnproto2/encoding/text"
-	schemas "zombiezen.com/go/capnproto2/schemas"
-)
-
-type Film struct{ capnp.Struct }
-
-// Film_TypeID is the unique identifier for the type Film.
-const Film_TypeID = 0x8662baafda730202
-
-func NewFilm(s *capnp.Segment) (Film, error) {
-	st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 32, PointerCount: 11})
-	return Film{st}, err
-}
-
-func NewRootFilm(s *capnp.Segment) (Film, error) {
-	st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 32, PointerCount: 11})
-	return Film{st}, err
-}
-
-func ReadRootFilm(msg *capnp.Message) (Film, error) {
-	root, err := msg.RootPtr()
-	return Film{root.Struct()}, err
-}
-
-func (s Film) String() string {
-	str, _ := text.Marshal(0x8662baafda730202, s.Struct)
-	return str
-}
-
-func (s Film) Id() uint64 {
-	return s.Struct.Uint64(0)
-}
-
-func (s Film) SetId(v uint64) {
-	s.Struct.SetUint64(0, v)
-}
-
-func (s Film) Title() (string, error) {
-	p, err := s.Struct.Ptr(0)
-	return p.Text(), err
-}
-
-func (s Film) HasTitle() bool {
-	p, err := s.Struct.Ptr(0)
-	return p.IsValid() || err != nil
-}
-
-func (s Film) TitleBytes() ([]byte, error) {
-	p, err := s.Struct.Ptr(0)
-	return p.TextBytes(), err
-}
-
-func (s Film) SetTitle(v string) error {
-	return s.Struct.SetText(0, v)
-}
-
-func (s Film) Genres() (capnp.TextList, error) {
-	p, err := s.Struct.Ptr(1)
-	return capnp.TextList{List: p.List()}, err
-}
-
-func (s Film) HasGenres() bool {
-	p, err := s.Struct.Ptr(1)
-	return p.IsValid() || err != nil
-}
-
-func (s Film) SetGenres(v capnp.TextList) error {
-	return s.Struct.SetPtr(1, v.List.ToPtr())
-}
-
-// NewGenres sets the genres field to a newly
-// allocated capnp.TextList, preferring placement in s's segment.
-func (s Film) NewGenres(n int32) (capnp.TextList, error) {
-	l, err := capnp.NewTextList(s.Struct.Segment(), n)
-	if err != nil {
-		return capnp.TextList{}, err
-	}
-	err = s.Struct.SetPtr(1, l.List.ToPtr())
-	return l, err
-}
-
-func (s Film) ReleaseDate() (Film_Date, error) {
-	p, err := s.Struct.Ptr(2)
-	return Film_Date{Struct: p.Struct()}, err
-}
-
-func (s Film) HasReleaseDate() bool {
-	p, err := s.Struct.Ptr(2)
-	return p.IsValid() || err != nil
-}
-
-func (s Film) SetReleaseDate(v Film_Date) error {
-	return s.Struct.SetPtr(2, v.Struct.ToPtr())
-}
-
-// NewReleaseDate sets the releaseDate field to a newly
-// allocated Film_Date struct, preferring placement in s's segment.
-func (s Film) NewReleaseDate() (Film_Date, error) {
-	ss, err := NewFilm_Date(s.Struct.Segment())
-	if err != nil {
-		return Film_Date{}, err
-	}
-	err = s.Struct.SetPtr(2, ss.Struct.ToPtr())
-	return ss, err
-}
-
-func (s Film) OriginalTitle() (string, error) {
-	p, err := s.Struct.Ptr(3)
-	return p.Text(), err
-}
-
-func (s Film) HasOriginalTitle() bool {
-	p, err := s.Struct.Ptr(3)
-	return p.IsValid() || err != nil
-}
-
-func (s Film) OriginalTitleBytes() ([]byte, error) {
-	p, err := s.Struct.Ptr(3)
-	return p.TextBytes(), err
-}
-
-func (s Film) SetOriginalTitle(v string) error {
-	return s.Struct.SetText(3, v)
-}
-
-func (s Film) Overview() (string, error) {
-	p, err := s.Struct.Ptr(4)
-	return p.Text(), err
-}
-
-func (s Film) HasOverview() bool {
-	p, err := s.Struct.Ptr(4)
-	return p.IsValid() || err != nil
-}
-
-func (s Film) OverviewBytes() ([]byte, error) {
-	p, err := s.Struct.Ptr(4)
-	return p.TextBytes(), err
-}
-
-func (s Film) SetOverview(v string) error {
-	return s.Struct.SetText(4, v)
-}
-
-func (s Film) Tagline() (string, error) {
-	p, err := s.Struct.Ptr(5)
-	return p.Text(), err
-}
-
-func (s Film) HasTagline() bool {
-	p, err := s.Struct.Ptr(5)
-	return p.IsValid() || err != nil
-}
-
-func (s Film) TaglineBytes() ([]byte, error) {
-	p, err := s.Struct.Ptr(5)
-	return p.TextBytes(), err
-}
-
-func (s Film) SetTagline(v string) error {
-	return s.Struct.SetText(5, v)
-}
-
-func (s Film) Runtime() uint16 {
-	return s.Struct.Uint16(8)
-}
-
-func (s Film) SetRuntime(v uint16) {
-	s.Struct.SetUint16(8, v)
-}
-
-func (s Film) IsAdult() bool {
-	return s.Struct.Bit(80)
-}
-
-func (s Film) SetIsAdult(v bool) {
-	s.Struct.SetBit(80, v)
-}
-
-func (s Film) Mark() float32 {
-	return math.Float32frombits(s.Struct.Uint32(12))
-}
-
-func (s Film) SetMark(v float32) {
-	s.Struct.SetUint32(12, math.Float32bits(v))
-}
-
-func (s Film) VoteCount() uint64 {
-	return s.Struct.Uint64(16)
-}
-
-func (s Film) SetVoteCount(v uint64) {
-	s.Struct.SetUint64(16, v)
-}
-
-func (s Film) Status() Film_Status {
-	return Film_Status(s.Struct.Uint16(24))
-}
-
-func (s Film) SetStatus(v Film_Status) {
-	s.Struct.SetUint16(24, uint16(v))
-}
-
-func (s Film) PosterPath() (string, error) {
-	p, err := s.Struct.Ptr(6)
-	return p.Text(), err
-}
-
-func (s Film) HasPosterPath() bool {
-	p, err := s.Struct.Ptr(6)
-	return p.IsValid() || err != nil
-}
-
-func (s Film) PosterPathBytes() ([]byte, error) {
-	p, err := s.Struct.Ptr(6)
-	return p.TextBytes(), err
-}
-
-func (s Film) SetPosterPath(v string) error {
-	return s.Struct.SetText(6, v)
-}
-
-func (s Film) BackdropPath() (string, error) {
-	p, err := s.Struct.Ptr(7)
-	return p.Text(), err
-}
-
-func (s Film) HasBackdropPath() bool {
-	p, err := s.Struct.Ptr(7)
-	return p.IsValid() || err != nil
-}
-
-func (s Film) BackdropPathBytes() ([]byte, error) {
-	p, err := s.Struct.Ptr(7)
-	return p.TextBytes(), err
-}
-
-func (s Film) SetBackdropPath(v string) error {
-	return s.Struct.SetText(7, v)
-}
-
-func (s Film) Collection() (Film_Collection, error) {
-	p, err := s.Struct.Ptr(8)
-	return Film_Collection{Struct: p.Struct()}, err
-}
-
-func (s Film) HasCollection() bool {
-	p, err := s.Struct.Ptr(8)
-	return p.IsValid() || err != nil
-}
-
-func (s Film) SetCollection(v Film_Collection) error {
-	return s.Struct.SetPtr(8, v.Struct.ToPtr())
-}
-
-// NewCollection sets the collection field to a newly
-// allocated Film_Collection struct, preferring placement in s's segment.
-func (s Film) NewCollection() (Film_Collection, error) {
-	ss, err := NewFilm_Collection(s.Struct.Segment())
-	if err != nil {
-		return Film_Collection{}, err
-	}
-	err = s.Struct.SetPtr(8, ss.Struct.ToPtr())
-	return ss, err
-}
-
-func (s Film) Cast() (Film_Character_List, error) {
-	p, err := s.Struct.Ptr(9)
-	return Film_Character_List{List: p.List()}, err
-}
-
-func (s Film) HasCast() bool {
-	p, err := s.Struct.Ptr(9)
-	return p.IsValid() || err != nil
-}
-
-func (s Film) SetCast(v Film_Character_List) error {
-	return s.Struct.SetPtr(9, v.List.ToPtr())
-}
-
-// NewCast sets the cast field to a newly
-// allocated Film_Character_List, preferring placement in s's segment.
-func (s Film) NewCast(n int32) (Film_Character_List, error) {
-	l, err := NewFilm_Character_List(s.Struct.Segment(), n)
-	if err != nil {
-		return Film_Character_List{}, err
-	}
-	err = s.Struct.SetPtr(9, l.List.ToPtr())
-	return l, err
-}
-
-func (s Film) Crew() (Film_CrewMember_List, error) {
-	p, err := s.Struct.Ptr(10)
-	return Film_CrewMember_List{List: p.List()}, err
-}
-
-func (s Film) HasCrew() bool {
-	p, err := s.Struct.Ptr(10)
-	return p.IsValid() || err != nil
-}
-
-func (s Film) SetCrew(v Film_CrewMember_List) error {
-	return s.Struct.SetPtr(10, v.List.ToPtr())
-}
-
-// NewCrew sets the crew field to a newly
-// allocated Film_CrewMember_List, preferring placement in s's segment.
-func (s Film) NewCrew(n int32) (Film_CrewMember_List, error) {
-	l, err := NewFilm_CrewMember_List(s.Struct.Segment(), n)
-	if err != nil {
-		return Film_CrewMember_List{}, err
-	}
-	err = s.Struct.SetPtr(10, l.List.ToPtr())
-	return l, err
-}
-
-// Film_List is a list of Film.
-type Film_List struct{ capnp.List }
-
-// NewFilm creates a new list of Film.
-func NewFilm_List(s *capnp.Segment, sz int32) (Film_List, error) {
-	l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 32, PointerCount: 11}, sz)
-	return Film_List{l}, err
-}
-
-func (s Film_List) At(i int) Film { return Film{s.List.Struct(i)} }
-
-func (s Film_List) Set(i int, v Film) error { return s.List.SetStruct(i, v.Struct) }
-
-func (s Film_List) String() string {
-	str, _ := text.MarshalList(0x8662baafda730202, s.List)
-	return str
-}
-
-// Film_Promise is a wrapper for a Film promised by a client call.
-type Film_Promise struct{ *capnp.Pipeline }
-
-func (p Film_Promise) Struct() (Film, error) {
-	s, err := p.Pipeline.Struct()
-	return Film{s}, err
-}
-
-func (p Film_Promise) ReleaseDate() Film_Date_Promise {
-	return Film_Date_Promise{Pipeline: p.Pipeline.GetPipeline(2)}
-}
-
-func (p Film_Promise) Collection() Film_Collection_Promise {
-	return Film_Collection_Promise{Pipeline: p.Pipeline.GetPipeline(8)}
-}
-
-type Film_Date struct{ capnp.Struct }
-
-// Film_Date_TypeID is the unique identifier for the type Film_Date.
-const Film_Date_TypeID = 0xc1264952b8db1201
-
-func NewFilm_Date(s *capnp.Segment) (Film_Date, error) {
-	st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 0})
-	return Film_Date{st}, err
-}
-
-func NewRootFilm_Date(s *capnp.Segment) (Film_Date, error) {
-	st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 0})
-	return Film_Date{st}, err
-}
-
-func ReadRootFilm_Date(msg *capnp.Message) (Film_Date, error) {
-	root, err := msg.RootPtr()
-	return Film_Date{root.Struct()}, err
-}
-
-func (s Film_Date) String() string {
-	str, _ := text.Marshal(0xc1264952b8db1201, s.Struct)
-	return str
-}
-
-func (s Film_Date) Year() int16 {
-	return int16(s.Struct.Uint16(0))
-}
-
-func (s Film_Date) SetYear(v int16) {
-	s.Struct.SetUint16(0, uint16(v))
-}
-
-func (s Film_Date) Month() uint8 {
-	return s.Struct.Uint8(2)
-}
-
-func (s Film_Date) SetMonth(v uint8) {
-	s.Struct.SetUint8(2, v)
-}
-
-func (s Film_Date) Day() uint8 {
-	return s.Struct.Uint8(3)
-}
-
-func (s Film_Date) SetDay(v uint8) {
-	s.Struct.SetUint8(3, v)
-}
-
-// Film_Date_List is a list of Film_Date.
-type Film_Date_List struct{ capnp.List }
-
-// NewFilm_Date creates a new list of Film_Date.
-func NewFilm_Date_List(s *capnp.Segment, sz int32) (Film_Date_List, error) {
-	l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 8, PointerCount: 0}, sz)
-	return Film_Date_List{l}, err
-}
-
-func (s Film_Date_List) At(i int) Film_Date { return Film_Date{s.List.Struct(i)} }
-
-func (s Film_Date_List) Set(i int, v Film_Date) error { return s.List.SetStruct(i, v.Struct) }
-
-func (s Film_Date_List) String() string {
-	str, _ := text.MarshalList(0xc1264952b8db1201, s.List)
-	return str
-}
-
-// Film_Date_Promise is a wrapper for a Film_Date promised by a client call.
-type Film_Date_Promise struct{ *capnp.Pipeline }
-
-func (p Film_Date_Promise) Struct() (Film_Date, error) {
-	s, err := p.Pipeline.Struct()
-	return Film_Date{s}, err
-}
-
-type Film_Status uint16
-
-// Film_Status_TypeID is the unique identifier for the type Film_Status.
-const Film_Status_TypeID = 0xbacdba448901b941
-
-// Values of Film_Status.
-const (
-	Film_Status_rumored        Film_Status = 0
-	Film_Status_planned        Film_Status = 1
-	Film_Status_inProduction   Film_Status = 2
-	Film_Status_postProduction Film_Status = 3
-	Film_Status_released       Film_Status = 4
-	Film_Status_cancelled      Film_Status = 5
-)
-
-// String returns the enum's constant name.
-func (c Film_Status) String() string {
-	switch c {
-	case Film_Status_rumored:
-		return "Rumored"
-	case Film_Status_planned:
-		return "Planned"
-	case Film_Status_inProduction:
-		return "In Production"
-	case Film_Status_postProduction:
-		return "Post Production"
-	case Film_Status_released:
-		return "Released"
-	case Film_Status_cancelled:
-		return "Cancelled"
-
-	default:
-		return ""
-	}
-}
-
-// Film_StatusFromString returns the enum value with a name,
-// or the zero value if there's no such value.
-func Film_StatusFromString(c string) Film_Status {
-	switch c {
-	case "Rumored":
-		return Film_Status_rumored
-	case "Planned":
-		return Film_Status_planned
-	case "In Production":
-		return Film_Status_inProduction
-	case "Post Production":
-		return Film_Status_postProduction
-	case "Released":
-		return Film_Status_released
-	case "Cancelled":
-		return Film_Status_cancelled
-
-	default:
-		return 0
-	}
-}
-
-type Film_Status_List struct{ capnp.List }
-
-func NewFilm_Status_List(s *capnp.Segment, sz int32) (Film_Status_List, error) {
-	l, err := capnp.NewUInt16List(s, sz)
-	return Film_Status_List{l.List}, err
-}
-
-func (l Film_Status_List) At(i int) Film_Status {
-	ul := capnp.UInt16List{List: l.List}
-	return Film_Status(ul.At(i))
-}
-
-func (l Film_Status_List) Set(i int, v Film_Status) {
-	ul := capnp.UInt16List{List: l.List}
-	ul.Set(i, uint16(v))
-}
-
-type Film_Collection struct{ capnp.Struct }
-
-// Film_Collection_TypeID is the unique identifier for the type Film_Collection.
-const Film_Collection_TypeID = 0xd4e4ef9e83a4bc53
-
-func NewFilm_Collection(s *capnp.Segment) (Film_Collection, error) {
-	st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2})
-	return Film_Collection{st}, err
-}
-
-func NewRootFilm_Collection(s *capnp.Segment) (Film_Collection, error) {
-	st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2})
-	return Film_Collection{st}, err
-}
-
-func ReadRootFilm_Collection(msg *capnp.Message) (Film_Collection, error) {
-	root, err := msg.RootPtr()
-	return Film_Collection{root.Struct()}, err
-}
-
-func (s Film_Collection) String() string {
-	str, _ := text.Marshal(0xd4e4ef9e83a4bc53, s.Struct)
-	return str
-}
-
-func (s Film_Collection) Name() (string, error) {
-	p, err := s.Struct.Ptr(0)
-	return p.Text(), err
-}
-
-func (s Film_Collection) HasName() bool {
-	p, err := s.Struct.Ptr(0)
-	return p.IsValid() || err != nil
-}
-
-func (s Film_Collection) NameBytes() ([]byte, error) {
-	p, err := s.Struct.Ptr(0)
-	return p.TextBytes(), err
-}
-
-func (s Film_Collection) SetName(v string) error {
-	return s.Struct.SetText(0, v)
-}
-
-func (s Film_Collection) Parts() (Film_Collection_CollectionPart_List, error) {
-	p, err := s.Struct.Ptr(1)
-	return Film_Collection_CollectionPart_List{List: p.List()}, err
-}
-
-func (s Film_Collection) HasParts() bool {
-	p, err := s.Struct.Ptr(1)
-	return p.IsValid() || err != nil
-}
-
-func (s Film_Collection) SetParts(v Film_Collection_CollectionPart_List) error {
-	return s.Struct.SetPtr(1, v.List.ToPtr())
-}
-
-// NewParts sets the parts field to a newly
-// allocated Film_Collection_CollectionPart_List, preferring placement in s's segment.
-func (s Film_Collection) NewParts(n int32) (Film_Collection_CollectionPart_List, error) {
-	l, err := NewFilm_Collection_CollectionPart_List(s.Struct.Segment(), n)
-	if err != nil {
-		return Film_Collection_CollectionPart_List{}, err
-	}
-	err = s.Struct.SetPtr(1, l.List.ToPtr())
-	return l, err
-}
-
-// Film_Collection_List is a list of Film_Collection.
-type Film_Collection_List struct{ capnp.List }
-
-// NewFilm_Collection creates a new list of Film_Collection.
-func NewFilm_Collection_List(s *capnp.Segment, sz int32) (Film_Collection_List, error) {
-	l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}, sz)
-	return Film_Collection_List{l}, err
-}
-
-func (s Film_Collection_List) At(i int) Film_Collection { return Film_Collection{s.List.Struct(i)} }
-
-func (s Film_Collection_List) Set(i int, v Film_Collection) error {
-	return s.List.SetStruct(i, v.Struct)
-}
-
-func (s Film_Collection_List) String() string {
-	str, _ := text.MarshalList(0xd4e4ef9e83a4bc53, s.List)
-	return str
-}
-
-// Film_Collection_Promise is a wrapper for a Film_Collection promised by a client call.
-type Film_Collection_Promise struct{ *capnp.Pipeline }
-
-func (p Film_Collection_Promise) Struct() (Film_Collection, error) {
-	s, err := p.Pipeline.Struct()
-	return Film_Collection{s}, err
-}
-
-type Film_Collection_CollectionPart struct{ capnp.Struct }
-
-// Film_Collection_CollectionPart_TypeID is the unique identifier for the type Film_Collection_CollectionPart.
-const Film_Collection_CollectionPart_TypeID = 0xa0d6cbb1ccac3e25
-
-func NewFilm_Collection_CollectionPart(s *capnp.Segment) (Film_Collection_CollectionPart, error) {
-	st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 3})
-	return Film_Collection_CollectionPart{st}, err
-}
-
-func NewRootFilm_Collection_CollectionPart(s *capnp.Segment) (Film_Collection_CollectionPart, error) {
-	st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 3})
-	return Film_Collection_CollectionPart{st}, err
-}
-
-func ReadRootFilm_Collection_CollectionPart(msg *capnp.Message) (Film_Collection_CollectionPart, error) {
-	root, err := msg.RootPtr()
-	return Film_Collection_CollectionPart{root.Struct()}, err
-}
-
-func (s Film_Collection_CollectionPart) String() string {
-	str, _ := text.Marshal(0xa0d6cbb1ccac3e25, s.Struct)
-	return str
-}
-
-func (s Film_Collection_CollectionPart) Id() uint64 {
-	return s.Struct.Uint64(0)
-}
-
-func (s Film_Collection_CollectionPart) SetId(v uint64) {
-	s.Struct.SetUint64(0, v)
-}
-
-func (s Film_Collection_CollectionPart) Title() (string, error) {
-	p, err := s.Struct.Ptr(0)
-	return p.Text(), err
-}
-
-func (s Film_Collection_CollectionPart) HasTitle() bool {
-	p, err := s.Struct.Ptr(0)
-	return p.IsValid() || err != nil
-}
-
-func (s Film_Collection_CollectionPart) TitleBytes() ([]byte, error) {
-	p, err := s.Struct.Ptr(0)
-	return p.TextBytes(), err
-}
-
-func (s Film_Collection_CollectionPart) SetTitle(v string) error {
-	return s.Struct.SetText(0, v)
-}
-
-func (s Film_Collection_CollectionPart) ReleaseDate() (Film_Date, error) {
-	p, err := s.Struct.Ptr(1)
-	return Film_Date{Struct: p.Struct()}, err
-}
-
-func (s Film_Collection_CollectionPart) HasReleaseDate() bool {
-	p, err := s.Struct.Ptr(1)
-	return p.IsValid() || err != nil
-}
-
-func (s Film_Collection_CollectionPart) SetReleaseDate(v Film_Date) error {
-	return s.Struct.SetPtr(1, v.Struct.ToPtr())
-}
-
-// NewReleaseDate sets the releaseDate field to a newly
-// allocated Film_Date struct, preferring placement in s's segment.
-func (s Film_Collection_CollectionPart) NewReleaseDate() (Film_Date, error) {
-	ss, err := NewFilm_Date(s.Struct.Segment())
-	if err != nil {
-		return Film_Date{}, err
-	}
-	err = s.Struct.SetPtr(1, ss.Struct.ToPtr())
-	return ss, err
-}
-
-func (s Film_Collection_CollectionPart) PosterPath() (string, error) {
-	p, err := s.Struct.Ptr(2)
-	return p.Text(), err
-}
-
-func (s Film_Collection_CollectionPart) HasPosterPath() bool {
-	p, err := s.Struct.Ptr(2)
-	return p.IsValid() || err != nil
-}
-
-func (s Film_Collection_CollectionPart) PosterPathBytes() ([]byte, error) {
-	p, err := s.Struct.Ptr(2)
-	return p.TextBytes(), err
-}
-
-func (s Film_Collection_CollectionPart) SetPosterPath(v string) error {
-	return s.Struct.SetText(2, v)
-}
-
-// Film_Collection_CollectionPart_List is a list of Film_Collection_CollectionPart.
-type Film_Collection_CollectionPart_List struct{ capnp.List }
-
-// NewFilm_Collection_CollectionPart creates a new list of Film_Collection_CollectionPart.
-func NewFilm_Collection_CollectionPart_List(s *capnp.Segment, sz int32) (Film_Collection_CollectionPart_List, error) {
-	l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 8, PointerCount: 3}, sz)
-	return Film_Collection_CollectionPart_List{l}, err
-}
-
-func (s Film_Collection_CollectionPart_List) At(i int) Film_Collection_CollectionPart {
-	return Film_Collection_CollectionPart{s.List.Struct(i)}
-}
-
-func (s Film_Collection_CollectionPart_List) Set(i int, v Film_Collection_CollectionPart) error {
-	return s.List.SetStruct(i, v.Struct)
-}
-
-func (s Film_Collection_CollectionPart_List) String() string {
-	str, _ := text.MarshalList(0xa0d6cbb1ccac3e25, s.List)
-	return str
-}
-
-// Film_Collection_CollectionPart_Promise is a wrapper for a Film_Collection_CollectionPart promised by a client call.
-type Film_Collection_CollectionPart_Promise struct{ *capnp.Pipeline }
-
-func (p Film_Collection_CollectionPart_Promise) Struct() (Film_Collection_CollectionPart, error) {
-	s, err := p.Pipeline.Struct()
-	return Film_Collection_CollectionPart{s}, err
-}
-
-func (p Film_Collection_CollectionPart_Promise) ReleaseDate() Film_Date_Promise {
-	return Film_Date_Promise{Pipeline: p.Pipeline.GetPipeline(1)}
-}
-
-type Film_Character struct{ capnp.Struct }
-
-// Film_Character_TypeID is the unique identifier for the type Film_Character.
-const Film_Character_TypeID = 0xfc58a269203af0a8
-
-func NewFilm_Character(s *capnp.Segment) (Film_Character, error) {
-	st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2})
-	return Film_Character{st}, err
-}
-
-func NewRootFilm_Character(s *capnp.Segment) (Film_Character, error) {
-	st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2})
-	return Film_Character{st}, err
-}
-
-func ReadRootFilm_Character(msg *capnp.Message) (Film_Character, error) {
-	root, err := msg.RootPtr()
-	return Film_Character{root.Struct()}, err
-}
-
-func (s Film_Character) String() string {
-	str, _ := text.Marshal(0xfc58a269203af0a8, s.Struct)
-	return str
-}
-
-func (s Film_Character) Person() (Person, error) {
-	p, err := s.Struct.Ptr(0)
-	return Person{Struct: p.Struct()}, err
-}
-
-func (s Film_Character) HasPerson() bool {
-	p, err := s.Struct.Ptr(0)
-	return p.IsValid() || err != nil
-}
-
-func (s Film_Character) SetPerson(v Person) error {
-	return s.Struct.SetPtr(0, v.Struct.ToPtr())
-}
-
-// NewPerson sets the person field to a newly
-// allocated Person struct, preferring placement in s's segment.
-func (s Film_Character) NewPerson() (Person, error) {
-	ss, err := NewPerson(s.Struct.Segment())
-	if err != nil {
-		return Person{}, err
-	}
-	err = s.Struct.SetPtr(0, ss.Struct.ToPtr())
-	return ss, err
-}
-
-func (s Film_Character) Name() (string, error) {
-	p, err := s.Struct.Ptr(1)
-	return p.Text(), err
-}
-
-func (s Film_Character) HasName() bool {
-	p, err := s.Struct.Ptr(1)
-	return p.IsValid() || err != nil
-}
-
-func (s Film_Character) NameBytes() ([]byte, error) {
-	p, err := s.Struct.Ptr(1)
-	return p.TextBytes(), err
-}
-
-func (s Film_Character) SetName(v string) error {
-	return s.Struct.SetText(1, v)
-}
-
-// Film_Character_List is a list of Film_Character.
-type Film_Character_List struct{ capnp.List }
-
-// NewFilm_Character creates a new list of Film_Character.
-func NewFilm_Character_List(s *capnp.Segment, sz int32) (Film_Character_List, error) {
-	l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}, sz)
-	return Film_Character_List{l}, err
-}
-
-func (s Film_Character_List) At(i int) Film_Character { return Film_Character{s.List.Struct(i)} }
-
-func (s Film_Character_List) Set(i int, v Film_Character) error { return s.List.SetStruct(i, v.Struct) }
-
-func (s Film_Character_List) String() string {
-	str, _ := text.MarshalList(0xfc58a269203af0a8, s.List)
-	return str
-}
-
-// Film_Character_Promise is a wrapper for a Film_Character promised by a client call.
-type Film_Character_Promise struct{ *capnp.Pipeline }
-
-func (p Film_Character_Promise) Struct() (Film_Character, error) {
-	s, err := p.Pipeline.Struct()
-	return Film_Character{s}, err
-}
-
-func (p Film_Character_Promise) Person() Person_Promise {
-	return Person_Promise{Pipeline: p.Pipeline.GetPipeline(0)}
-}
-
-type Film_CrewMember struct{ capnp.Struct }
-
-// Film_CrewMember_TypeID is the unique identifier for the type Film_CrewMember.
-const Film_CrewMember_TypeID = 0x8afd64c84282f606
-
-func NewFilm_CrewMember(s *capnp.Segment) (Film_CrewMember, error) {
-	st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2})
-	return Film_CrewMember{st}, err
-}
-
-func NewRootFilm_CrewMember(s *capnp.Segment) (Film_CrewMember, error) {
-	st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2})
-	return Film_CrewMember{st}, err
-}
-
-func ReadRootFilm_CrewMember(msg *capnp.Message) (Film_CrewMember, error) {
-	root, err := msg.RootPtr()
-	return Film_CrewMember{root.Struct()}, err
-}
-
-func (s Film_CrewMember) String() string {
-	str, _ := text.Marshal(0x8afd64c84282f606, s.Struct)
-	return str
-}
-
-func (s Film_CrewMember) Person() (Person, error) {
-	p, err := s.Struct.Ptr(0)
-	return Person{Struct: p.Struct()}, err
-}
-
-func (s Film_CrewMember) HasPerson() bool {
-	p, err := s.Struct.Ptr(0)
-	return p.IsValid() || err != nil
-}
-
-func (s Film_CrewMember) SetPerson(v Person) error {
-	return s.Struct.SetPtr(0, v.Struct.ToPtr())
-}
-
-// NewPerson sets the person field to a newly
-// allocated Person struct, preferring placement in s's segment.
-func (s Film_CrewMember) NewPerson() (Person, error) {
-	ss, err := NewPerson(s.Struct.Segment())
-	if err != nil {
-		return Person{}, err
-	}
-	err = s.Struct.SetPtr(0, ss.Struct.ToPtr())
-	return ss, err
-}
-
-func (s Film_CrewMember) Job() (string, error) {
-	p, err := s.Struct.Ptr(1)
-	return p.Text(), err
-}
-
-func (s Film_CrewMember) HasJob() bool {
-	p, err := s.Struct.Ptr(1)
-	return p.IsValid() || err != nil
-}
-
-func (s Film_CrewMember) JobBytes() ([]byte, error) {
-	p, err := s.Struct.Ptr(1)
-	return p.TextBytes(), err
-}
-
-func (s Film_CrewMember) SetJob(v string) error {
-	return s.Struct.SetText(1, v)
-}
-
-// Film_CrewMember_List is a list of Film_CrewMember.
-type Film_CrewMember_List struct{ capnp.List }
-
-// NewFilm_CrewMember creates a new list of Film_CrewMember.
-func NewFilm_CrewMember_List(s *capnp.Segment, sz int32) (Film_CrewMember_List, error) {
-	l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}, sz)
-	return Film_CrewMember_List{l}, err
-}
-
-func (s Film_CrewMember_List) At(i int) Film_CrewMember { return Film_CrewMember{s.List.Struct(i)} }
-
-func (s Film_CrewMember_List) Set(i int, v Film_CrewMember) error {
-	return s.List.SetStruct(i, v.Struct)
-}
-
-func (s Film_CrewMember_List) String() string {
-	str, _ := text.MarshalList(0x8afd64c84282f606, s.List)
-	return str
-}
-
-// Film_CrewMember_Promise is a wrapper for a Film_CrewMember promised by a client call.
-type Film_CrewMember_Promise struct{ *capnp.Pipeline }
-
-func (p Film_CrewMember_Promise) Struct() (Film_CrewMember, error) {
-	s, err := p.Pipeline.Struct()
-	return Film_CrewMember{s}, err
-}
-
-func (p Film_CrewMember_Promise) Person() Person_Promise {
-	return Person_Promise{Pipeline: p.Pipeline.GetPipeline(0)}
-}
-
-type Person struct{ capnp.Struct }
-
-// Person_TypeID is the unique identifier for the type Person.
-const Person_TypeID = 0xbe38be3daa02cbbb
-
-func NewPerson(s *capnp.Segment) (Person, error) {
-	st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 2})
-	return Person{st}, err
-}
-
-func NewRootPerson(s *capnp.Segment) (Person, error) {
-	st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 2})
-	return Person{st}, err
-}
-
-func ReadRootPerson(msg *capnp.Message) (Person, error) {
-	root, err := msg.RootPtr()
-	return Person{root.Struct()}, err
-}
-
-func (s Person) String() string {
-	str, _ := text.Marshal(0xbe38be3daa02cbbb, s.Struct)
-	return str
-}
-
-func (s Person) Id() uint64 {
-	return s.Struct.Uint64(0)
-}
-
-func (s Person) SetId(v uint64) {
-	s.Struct.SetUint64(0, v)
-}
-
-func (s Person) Name() (string, error) {
-	p, err := s.Struct.Ptr(0)
-	return p.Text(), err
-}
-
-func (s Person) HasName() bool {
-	p, err := s.Struct.Ptr(0)
-	return p.IsValid() || err != nil
-}
-
-func (s Person) NameBytes() ([]byte, error) {
-	p, err := s.Struct.Ptr(0)
-	return p.TextBytes(), err
-}
-
-func (s Person) SetName(v string) error {
-	return s.Struct.SetText(0, v)
-}
-
-func (s Person) PhotoPath() (string, error) {
-	p, err := s.Struct.Ptr(1)
-	return p.Text(), err
-}
-
-func (s Person) HasPhotoPath() bool {
-	p, err := s.Struct.Ptr(1)
-	return p.IsValid() || err != nil
-}
-
-func (s Person) PhotoPathBytes() ([]byte, error) {
-	p, err := s.Struct.Ptr(1)
-	return p.TextBytes(), err
-}
-
-func (s Person) SetPhotoPath(v string) error {
-	return s.Struct.SetText(1, v)
-}
-
-// Person_List is a list of Person.
-type Person_List struct{ capnp.List }
-
-// NewPerson creates a new list of Person.
-func NewPerson_List(s *capnp.Segment, sz int32) (Person_List, error) {
-	l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 8, PointerCount: 2}, sz)
-	return Person_List{l}, err
-}
-
-func (s Person_List) At(i int) Person { return Person{s.List.Struct(i)} }
-
-func (s Person_List) Set(i int, v Person) error { return s.List.SetStruct(i, v.Struct) }
-
-func (s Person_List) String() string {
-	str, _ := text.MarshalList(0xbe38be3daa02cbbb, s.List)
-	return str
-}
-
-// Person_Promise is a wrapper for a Person promised by a client call.
-type Person_Promise struct{ *capnp.Pipeline }
-
-func (p Person_Promise) Struct() (Person, error) {
-	s, err := p.Pipeline.Struct()
-	return Person{s}, err
-}
-
-const schema_cd7129bab3129d29 = "x\xda\x8c\x96\xef\x8bSW\x1e\xc6\x9f\xe7\x9c$w\x12" +
-	"\xe3$\xd7{\x97\xdd\x11%\xee\xa2\x8b\x0e\xab\x8c\xa3." +
-	"2\xb0;\xce\x8f]vD!7\x11V\x86\x15\xbc\x93" +
-	"\\\x9ch\x92\x1bon\x1c\x94]\x86ua\xa1\xfe\x03" +
-	"\x85\x82\xa5-\x15\xdaR\x90\xfex\xd1N\x8b\xc5B\xa9" +
-	"T\x85\x0aZ\x8a}'-\xadP\xd1B-\x15,\xb7" +
-	"\x9c\x9b\x1f7S\xa7\xea\xbb\xc3'\xdfs\xcf\xf3=\xe7" +
-	"y\xce\xc9\xc8\x9f\xe5\x1e\xb1=\xbe\x18\x03\xac\x9d\xf1D" +
-	" D\xf3\xe6\xf9\xa5\xb9\xff\xc3J3\x16l9\xbb\xe6" +
-	"\xad\xa5-\xc7\xaf\"\xbeJ\x03\x8cM\xe2\x8c\xb1U\xfc" +
-	"\x13\xd8q\\| \xc1 \xf1\xc3\xe9\xc9K\xe5\x9f\xce" +
-	"@\xd7\x19M\x8d\x0bU|6q\xcbx5\xa1F\xe7" +
-	"\x12\x0b`\xb0\xe9\xaf\xaf_y\xe3\xf2g/\xc0ZG" +
-	"\x06\xc5\xf7_\xfe\xdf\xf3w\xbf\xbc\x8e\xb8\xd4\x80\x1d\xd4" +
-	"fi\xfcFS\xd5\xba\xf65\x18L\xbc\xcbg\xa6\x97" +
-	"\xae.A\xd7E\xf4e\xd0\xb8\xa3}h\xdc\x0f\x0b\xbf" +
-	"\xd3\x9e\x05\x83\xf7.\x8b\xd7\xfera\xf7\x05%\x98}" +
-	"\x82C\x0d[\x93\xcf\x19\xbb\x92j\xb4=y\x1e\x0c\xb8" +
-	"\xe6\x8bw\x0a3\x7f\xbc\x08Kg\x9f\xe0\x98\xaa\xb8\x9a" +
-	"|\xd3\xb8\x11\xd6^\x0bk{\x0aW\xe8\xad\x92\xbae" +
-	"\xb4R\xbf\x05\x8c\xff\xa6To\xaf\xdc\x1b\xdbPy\xe9" +
-	"\xe0\xc3\x95j\xef\xa4n\x1a\x0fRjt?\xb5\x80\xdd" +
-	"\x81]k5\x9dm\x0d\x8f\xae\xef\x8e\xfd\xbdR\xad\x01" +
-	"\x96\xc9>m\xfa\xef\x87\xfb\xda_?\xd6\xa7dh\xb6" +
-	"o\xa9\xa1B\xdf\xfe\x0f\xcdf\xa6m\xdf\x19/\xfa\xb6" +
-	"\xdfj\x06Sn\xb5\xea\x94\xfc\x0a\xa4[\x0f\xa6\xe6m" +
-	"\xcf.\xf9\x0e\xe8\x05S\x9e\xb3\xb0\xdf\xa9\xcdA:\x9e" +
-	"5-c@\x8c\xaay\xae\x05\x8a\x97(Y\xbcNA" +
-	"\xd2\xa4\xc2\xd78\x0a\x14\xaf(\xfc9\x05uA\x93\x02" +
-	"0np\x0c(~\xaa\xf8\xb7\x8aKaR\x02\xc6m" +
-	"\xce\x01\xc5o\x14\xff^\xf1\x984\x19SGE\x0f(" +
-	"\xdeS\xfc\xa1\xe2\xf1\x98\xc98`<\xe0^\xa0\xf8\xa3" +
-	"\xe21!\xa8'\xe2&\x13\x80A1\x09\x14\x1f*>" +
-	"\xa0\xb8\x163\xa9v0\xaexAH\x16\xd3\x0a\x0f\xe4" +
-	"M\x0e\x00F2,\x8f)\x9eU<)M&\x01c" +
-	"\xb5\x18\x06\x8a\x03\x8a\x9b\x8a\xa7\x84\xc9\x94\xb2\x98(\x00" +
-	"\xc5\xac\xe2\xeb\x14_\x956\xb9\x0a0\x86\x84j\xcbT" +
-	"|\x83\xe2\xe9\x84\xc94`\xac\x17\xb3@q\x9d\xe2\x9b" +
-	"\x15_\xad\x99\\\x1d\xe6\xe1(P\xdc\xa8\xf8\x88\xe2\x83" +
-	"\x03&\x07\x95\xed\xc2\xfa?)\xbe[\xf1L\xd2d\x06" +
-	"0v\x85zF\x14\xdf\xa7x6e2\x0b\x183!" +
-	"\x9fV\xfc\xb0\x10\x94\x952\x93\x10L\x829\xbf\xe2W" +
-	"\x1d\xa6!\x98\x06\xc7\x8f8u\xcfir\x10\xccK\x86" +
-	"t\x10\x0c<\xa7\xea\xd8Mg\x1a\x9a\xed;\xccFN" +
-	"\x02\x99\x05\x03\xd7\xab\x1c\xa9\xd4\xed*r\x07\xfa\xbf\x16" +
-	"\xb8'\x1c\xefD\xc5Y\x00\xd0e\x8b\xbe}\xa4Z\xa9" +
-	"\xf7j\x16\xbdV\xdd\xaf\xd4\x1cj\x10\xd4\xc0\xc5Js" +
-	"\xa2\xdc\xaa\xfa$\x04\x09fj\xb6w\x8c)\x08\xa6\xc0" +
-	"\xe0\x84\xeb;Sn\xab\x0e\xfa\xdd\x06\xc6\x9b\xa1!\x99" +
-	"\x89,\x0d2\x03\x06\x0d\xb7\xe9;^\xde\x86\xf4\xe7{" +
-	"\x92\xe6\xec\xd2\xb1\xb2\xe76\x90\xc9\xdb}\xb8\xd4gg" +
-	"f\xa3@\xb4\x1b\xcc\x94\xec\xa6\xdf\xdd\x94l\x94\x10P" +
-	"\xc1L\xc9s\x16\xa2_{\x91i\xff\xda\x8b\xa3\xe8\xc6" +
-	"q['\"\xda\x9c\xe3\xe5Ik\xa0\x1b\x12}\xcb\x18" +
-	"`m\x94\xb4F\x04\xf5ND\xf4\xad\x7f\x00\xac\xcd\x92" +
-	"\xd6N\xc1\xf1\x86\xe35C\x85\xbd[\xa9\xadP;\xea" +
-	"\xce\xf5\x9a\xe9\xae\x18\x8bV\xec\xb4\xe7\xd6\xfb\x86y[" +
-	"z\xbe\x12\x90\xed\x09\xb0\xd7\x02\xd6\xbf$\xad\xf9^D" +
-	"ug\x14\xb0\x0eKZ\xd5(\x9fze\x0e\xb0\xe6%" +
-	"-?\x0a\xa7~|\x16\xb0\x1a\x92\xd6\xbf\x1fc\xb1'" +
-	"\x9ai\xc5c{t\x13\xdb\xf7\x10\x94\xfe\xdf\x85\x92&" +
-	"&\xf5\x89\x1c\xa9\x1f\x9a\xd4\x0f\xe5(\xf4\x93G\xf5\xff" +
-	"\xe4(\xf5\xb3\xa7\xf4\x17s\x8c\xe9\x17\xf7\xea\x1f\xe5\x18" +
-	"\xd7\xbf*\xe8\xb7s\x8b^\xab\xe6zN\xd9\x8aQ\x04" +
-	"\x1f\xdf\xfdd\xe3\xd0\xdb\xfe9X1\xc1\x89\xac\xf2=" +
-	"tN.\x16\xda5\xc0b\xa3j\xd7\xebO\xa8\xce\xb7" +
-	"k\x80\xa0R\xcf{n\xb9UBFm\xf3\x0a\x93\xcc" +
-	"\xce$/\x98\xa9o\x08k\x91\x0b\x8f\x04\x08\xdbW\x08" +
-	"\xe3\xad\xd2\x13\xa6\x9f\x0e\xf2n\xd3W\x1f`\xb9\xd5\x99" +
-	"\x8f\xee\xfe\x96\x01<f\xee\xde\xa0\xd0\xabcP\xb2\xeb" +
-	"%\xa7Zu\xc0\xf2c\xe6\x14\x82\xa9\xa8\x0eX\xfe\xd0" +
-	"\xe4\x1d\xaf)\xdd\xba:\x8ft\xcfO\x7fS~\xda#" +
-	"i\xed\x8b\xfc43\x0cX\xd3\x92V\xbe\xcfO\xfb\x0b" +
-	"\x80\xb5O\xd2:\xb8\xcc:\x99\xba]\x8b\x9c\xd3\x98w" +
-	"}7o\xfb\xe0\xa3\xc6\xe8=v\xb9m\xea\x99\xfa\x85" +
-	"\x8c\xe1H\x86N\xd1\xd11\xda\xafCvt\xa8\xb0\xfd" +
-	"C\xd2: \x989\xe9\xd8\x1e%\x04%\x98\xab\xb9u" +
-	"\x7f\x9e\x09\x08&@\xadl\x9f\xec\x8eW\x0ax;c" +
-	"Z\xfb\xf8\xfa\xfe\x99\xe8<\x15\xbd\x9c\xe3*\x82\x9e\xdf" +
-	"\x9f\xff\xe1\x95\xf2?\xda\xc9\xff\xb4X\xbe\x1f\xb9\x86\xed" +
-	"\xf9\xcd\xe8\xe2\xe9\xad\xf2\xeb\x17O\xfb\x9d\x96Ow\xef" +
-	"\x0c?\xc5\xbd\xb3L\xd0\xcf\x01\x00\x00\xff\xff\xd0*V" +
-	"\x11"
-
-func init() {
-	schemas.Register(schema_cd7129bab3129d29,
-		0x8662baafda730202,
-		0x8afd64c84282f606,
-		0xa0d6cbb1ccac3e25,
-		0xbacdba448901b941,
-		0xbe38be3daa02cbbb,
-		0xc1264952b8db1201,
-		0xd4e4ef9e83a4bc53,
-		0xfc58a269203af0a8)
-}




diff --git a/protocol/film.go b/protocol/film.go
deleted file mode 100644
index 151dad703b033cc713a167040cb893268e7dd15b..0000000000000000000000000000000000000000
--- a/protocol/film.go
+++ /dev/null
@@ -1,100 +0,0 @@
-package protocol
-
-import (
-	"notabug.org/apiote/amuse/tmdb"
-	"notabug.org/apiote/amuse/utils"
-
-	"zombiezen.com/go/capnproto2"
-
-	"io"
-	"time"
-	"bytes"
-)
-
-const (
-	posterWidth   = "w154"
-	backdropWidth = "w1280"
-)
-
-
-func Recode(tmdbFilm *tmdb.Film, tmdbCollection *tmdb.Collection) (io.Writer, error) {
-	var err error = nil
-
-	msg, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
-	film, err2 := NewRootFilm(seg)
-	err = utils.Or(err, err2)
-
-	err = utils.Or(film.SetBackdropPath("https://image.tmdb.org/t/p/"+backdropWidth+tmdbFilm.Backdrop_path), err)
-
-	collection, err2 := film.NewCollection()
-	err = utils.Or(err, err2)
-	err = utils.Or(collection.SetName(tmdbCollection.Name), err)
-	parts, err2 := collection.NewParts(int32(len(tmdbCollection.Parts)))
-	err = utils.Or(err, err2)
-	for i, tmdbPart := range tmdbCollection.Parts {
-		part := parts.At(i)
-		part.SetId(uint64(tmdbPart.Id))
-		err = utils.Or(part.SetTitle(tmdbPart.Title), err)
-		err = utils.Or(part.SetPosterPath("https://image.tmdb.org/t/p/"+posterWidth+tmdbPart.Poster_path), err)
-		releaseDate, err2 := part.NewReleaseDate()
-		err = utils.Or(err, err2)
-		releaseDate.SetYear(int16(tmdbFilm.Release_date.Year()))
-		releaseDate.SetMonth(uint8(tmdbFilm.Release_date.Month()))
-		releaseDate.SetDay(uint8(tmdbFilm.Release_date.Day()))
-		err = utils.Or(part.SetReleaseDate(releaseDate), err)
-		//parts.Set(i, part)
-	}
-
-	genres, err2 := film.NewGenres(int32(len(tmdbFilm.Genres)))
-	err = utils.Or(err, err2)
-	for i, tmdbGenre := range tmdbFilm.Genres {
-		genres.Set(i, tmdbGenre.Name)
-	}
-
-	err = utils.Or(film.SetOriginalTitle(tmdbFilm.Original_title), err)
-	err = utils.Or(film.SetOverview(tmdbFilm.Overview), err)
-	err = utils.Or(film.SetPosterPath("https://image.tmdb.org/t/p/"+posterWidth+tmdbFilm.Poster_path), err)
-
-	releaseDate, err2 := film.NewReleaseDate()
-	err = utils.Or(err, err2)
-	date, err2 := time.Parse("2006-01-02", tmdbFilm.Release_date_str)
-	err = utils.Or(err, err2)
-	releaseDate.SetYear(int16(date.Year()))
-	releaseDate.SetMonth(uint8(date.Month()))
-	releaseDate.SetDay(uint8(date.Day()))
-	err = utils.Or(film.SetReleaseDate(releaseDate), err)
-
-	film.SetRuntime(uint16(tmdbFilm.Runtime))
-	film.SetStatus(Film_StatusFromString(tmdbFilm.Status))
-	err = utils.Or(film.SetTagline(tmdbFilm.Tagline), err)
-	err = utils.Or(film.SetTitle(tmdbFilm.Title), err)
-	film.SetMark(tmdbFilm.Vote_average)
-	film.SetVoteCount(uint64(tmdbFilm.Vote_count))
-
-	cast, err2 := film.NewCast(int32(len(tmdbFilm.Credits.Cast)))
-	err = utils.Or(err, err2)
-	for i, tmdbCharacter := range tmdbFilm.Credits.Cast {
-		character := cast.At(i)
-		err = utils.Or(character.SetName(tmdbCharacter.Character), err)
-		person, err2 := character.NewPerson()
-		err = utils.Or(err, err2)
-		person.SetId(uint64(tmdbCharacter.Id))
-		err = utils.Or(person.SetName(tmdbCharacter.Name), err)
-		err = utils.Or(person.SetPhotoPath("https://image.tmdb.org/t/p/"+posterWidth+tmdbCharacter.Profile_path), err)
-	}
-
-	crew, err2 := film.NewCrew(int32(len(tmdbFilm.Credits.Crew)))
-	err = utils.Or(err, err2)
-	for i, tmdbCrewMember := range tmdbFilm.Credits.Crew {
-		crewMember := crew.At(i)
-		err = utils.Or(crewMember.SetJob(tmdbCrewMember.Job), err)
-		person, err2 := crewMember.NewPerson()
-		err = utils.Or(err, err2)
-		person.SetId(uint64(tmdbCrewMember.Id))
-		err = utils.Or(person.SetName(tmdbCrewMember.Name), err)
-		err = utils.Or(person.SetPhotoPath("https://image.tmdb.org/t/p/"+posterWidth+tmdbCrewMember.Profile_path), err)
-	}
-	b := bytes.NewBuffer([]byte{})
-	err = utils.Or(capnp.NewEncoder(b).Encode(msg), err)
-	return b, err
-}




diff --git a/protocol/makefile b/protocol/makefile
deleted file mode 100644
index 84ebea804fd2a6c5163cab9fd24d6b3855055cbf..0000000000000000000000000000000000000000
--- a/protocol/makefile
+++ /dev/null
@@ -1,2 +0,0 @@
-amuse.proto.go: amuse.proto
-	capnp compile -I/usr/include/capnp/ -ogo amuse.proto




diff --git a/router.go b/router.go
index 1b0d39dec9c4039d2f14d50614d94bf7e611190a..a91c99047a40de7adf27ce158b1c5cac326afc05 100644
--- a/router.go
+++ b/router.go
@@ -6,7 +6,7 @@ 	"notabug.org/apiote/amuse/db"
 	"notabug.org/apiote/amuse/front"
 	"notabug.org/apiote/amuse/libamuse"
 	"notabug.org/apiote/amuse/network"
-	"notabug.org/apiote/amuse/utils"
+	"notabug.org/apiote/amuse/config"
 
 	"crypto/sha256"
 	"encoding/base64"
@@ -242,6 +242,13 @@ }
 
 func signupPost(w http.ResponseWriter, r *http.Request, acceptLanguages, mimetype string) {
 	// todo check mimetype (html,capnproto)
+
+	if !config.OpenRegistration {
+		err := errors.New("423")
+		render("", err, w, acceptLanguages, mimetype)
+		return
+	}
+
 	r.ParseForm()
 	username := r.PostForm.Get("username")
 	password := r.PostForm.Get("password")
@@ -302,7 +309,7 @@ }
 
 func static(w http.ResponseWriter, r *http.Request) {
 	etagReq := r.Header.Get("If-None-Match")
-	f, err := os.Open(utils.DataHome + "/" + r.URL.Path[1:])
+	f, err := os.Open(config.DataHome + "/" + r.URL.Path[1:])
 	if err != nil {
 		w.WriteHeader(500)
 		return
@@ -354,6 +361,20 @@ 	w.Header().Set("ETag", avatar.Etag)
 	w.Write(avatar.Data)
 }
 
+func addToWantlist(w http.ResponseWriter, r *http.Request, username string, auth accounts.Authentication, acceptLanguages, mimetype string) {
+	r.ParseForm()
+	itemId := r.PostForm.Get("itemId")
+	itemType := r.PostForm.Get("itemType")
+	target := "/" + itemType + "s/" + itemId
+	err := libamuse.AddToWantlist(username, auth, itemId, itemType, acceptLanguages, mimetype)
+	if err != nil {
+		render("", err, w, acceptLanguages, mimetype)
+	} else {
+		w.Header().Add("Location", target)
+		w.WriteHeader(303)
+	}
+}
+
 func userWatchlist(w http.ResponseWriter, r *http.Request, username string, auth accounts.Authentication, acceptLanguages string, mimetype string) {
 	if r.Method == "" || r.Method == "GET" {
 		var page int
@@ -363,17 +384,7 @@ 		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")
-		itemType := r.PostForm.Get("itemType")
-		target := "/" + itemType + "s/" + itemId
-		err := libamuse.AddToWantlist(username, auth, itemId, itemType, acceptLanguages, mimetype)
-		if err != nil {
-			render("", err, w, acceptLanguages, mimetype)
-		} else {
-			w.Header().Add("Location", target)
-			w.WriteHeader(303)
-		}
+		addToWantlist(w, r, username, auth, acceptLanguages, mimetype)
 	}
 }
 
@@ -398,6 +409,19 @@ 		} else {
 			w.Header().Add("Location", target)
 			w.WriteHeader(303)
 		}
+	}
+}
+
+func userReadlist(w http.ResponseWriter, r *http.Request, username string, auth accounts.Authentication, acceptLanguages string, mimetype string) {
+	if r.Method == "" || r.Method == "GET" {
+		var page int
+		r.ParseForm()
+		filter := r.Form.Get("filter")
+		fmt.Sscanf(r.Form.Get("page"), "%d", &page)
+		readlist, err := libamuse.ShowReadlist(username, auth, acceptLanguages, mimetype, filter, page)
+		render(readlist, err, w, acceptLanguages, mimetype)
+	} else if r.Method == "POST" {
+		addToWantlist(w, r, username, auth, acceptLanguages, mimetype)
 	}
 }
 
@@ -417,8 +441,8 @@ 		isOtherTime := r.PostForm.Get("isOtherTime") == "true"
 
 		var datetime string
 		if isOtherTime {
-			date := r.PostForm.Get("watchedDate")
-			time := r.PostForm.Get("watchedTime")
+			date := r.PostForm.Get("experiencedDate")
+			time := r.PostForm.Get("experiencedTime")
 			datetime = date + "T" + time + ":00"
 		} else {
 			datetime = ""
@@ -489,6 +513,8 @@ 		case "watchlist":
 			userWatchlist(w, r, username, auth, acceptLanguages, mimetype)
 		case "tvqueue":
 			userTvQueue(w, r, username, auth, acceptLanguages, mimetype)
+		case "readlist":
+			userReadlist(w, r, username, auth, acceptLanguages, mimetype)
 		case "experiences":
 			userExperiences(w, r, username, auth, acceptLanguages, mimetype)
 		case "sessions":
@@ -510,8 +536,8 @@ 	setAuthCookie(false, "", w)
 	render(loggedout, err, w, acceptLanguages, mimetype)
 }
 
-func route(port int) {
-	portStr := fmt.Sprintf(":%d", port)
+func route(port uint) {
+	address := fmt.Sprintf("%s:%d", config.Address, port)
 
 	http.HandleFunc("/", index)
 	http.HandleFunc("/static/", static)
@@ -528,8 +554,8 @@ 	http.HandleFunc("/login", login)
 	http.HandleFunc("/signup", signup)
 	http.HandleFunc("/signedup", signedup)
 	http.HandleFunc("/loggedout", loggedout)
-	fmt.Printf("running on %s\n", portStr)
-	e := http.ListenAndServe(portStr, nil)
+	fmt.Printf("running on %s\n", address)
+	e := http.ListenAndServe(address, nil)
 	if e != nil {
 		fmt.Println(e)
 	}
@@ -595,6 +621,8 @@ 				renderError(401, w, e, languages, mimetype)
 			} else {
 				renderError(403, w, e, languages, mimetype)
 			}
+		} else if e.Error() == "423" {
+			renderError(423, w, e, languages, mimetype)
 		} else {
 			renderError(500, w, e, languages, mimetype)
 		}




diff --git a/router.py b/router.py
deleted file mode 100644
index 946569c20ea17d550c400d1a307d73706898b32c..0000000000000000000000000000000000000000
--- a/router.py
+++ /dev/null
@@ -1,122 +0,0 @@
-#!/bin/python
-from flask import Flask, request
-import libamuse
-
-app = Flask(__name__)
-
-@app.errorhandler(404)
-def err_404(e):
-	languages = request.headers.get('Accept-Language', 'en-GB')
-	mimetype = request.headers.get('Accept', '').split(',')[0]
-	return libamuse.ShowErrorPage(404, languages, mimetype)
-
-@app.route('/')
-def index():
-	languages = request.headers.get('Accept-Language', 'en-GB')
-	mimetype = request.headers.get('Accept', '').split(',')[0]
-	try:
-		page = libamuse.ShowIndex(languages, mimetype)
-	except Exception as e:
-		try:
-			return libamuse.ShowErrorPage(int(str(e.__context__)), languages, mimetype)
-		except:
-			return f'Fatal error while rendering error {str(e.__context__)}.\nContact admin.', 500
-	else:
-		return page
-
-
-@app.route('/about')
-def about():
-	languages = request.headers.get('Accept-Language', 'en-GB')
-	mimetype = request.headers.get('Accept', '').split(',')[0]
-	try:
-		page = libamuse.ShowAbout(languages, mimetype)
-	except Exception as e:
-		try:
-			return libamuse.ShowErrorPage(int(str(e.__context__)), languages, mimetype)
-		except:
-			return f'Fatal error while rendering error {str(e.__context__)}.\nContact admin.', 500
-	else:
-		return page
-
-
-@app.route('/items/')
-def search():
-	languages = request.headers.get('Accept-Language', 'en-GB')
-	mimetype = request.headers.get('Accept', '').split(',')[0]
-	query = request.args.get('q', default='', type=str)
-	page = request.args.get('page', default=1, type=int)
-	try:
-		page = libamuse.PerformSearch(query, languages, mimetype, str(page))
-	except Exception as e:
-		try:
-			return libamuse.ShowErrorPage(int(str(e.__context__)), languages, mimetype)
-		except:
-			return f'Fatal error while rendering error {str(e.__context__)}.\nContact admin.', 500
-	else:
-		return page
-
-
-@app.route('/films/<id>')
-def film(id):
-	languages = request.headers.get('Accept-Language', 'en-GB')
-	mimetype = request.headers.get('Accept', '').split(',')[0]
-	etag = request.headers.get('ETag', '')
-	try:
-		intID = int(id)
-	except ValueError:
-		return libamuse.ShowErrorPage(400, languages, mimetype)
-	try:
-		page = libamuse.ShowFilm(id, etag, languages, mimetype)
-	except Exception as e:
-		try:
-			return libamuse.ShowErrorPage(int(str(e.__context__)), languages, mimetype)
-		except:
-			return f'Fatal error while rendering error {str(e.__context__)}.\nContact admin.', 500
-	else:
-		return page
-
-
-@app.route('/series/<id>')
-def serie(id):
-	languages = request.headers.get('Accept-Language', 'en-GB')
-	mimetype = request.headers.get('Accept', '').split(',')[0]
-	etag = request.headers.get('ETag', '')
-	try:
-		intID = int(id)
-	except ValueError:
-		return libamuse.ShowErrorPage(400, languages, mimetype)
-	try:
-		page = libamuse.ShowSerie(id, etag, languages, mimetype)
-	except Exception as e:
-		try:
-			return libamuse.ShowErrorPage(int(str(e.__context__)), languages, mimetype)
-		except:
-			return f'Fatal error while rendering error {str(e.__context__)}.\nContact admin.', 500
-	else:
-		return page
-
-
-@app.route('/persons/<id>')
-def person(id):
-	languages = request.headers.get('Accept-Language', 'en-GB')
-	mimetype = request.headers.get('Accept', '').split(',')[0]
-	etag = request.headers.get('ETag', '')
-	try:
-		intID = int(id)
-	except ValueError:
-		return libamuse.ShowErrorPage(400, languages, mimetype)
-	try:
-		page = libamuse.ShowPerson(id, etag, languages, mimetype)
-	except Exception as e:
-		try:
-			return libamuse.ShowErrorPage(int(str(e.__context__)), languages, mimetype)
-		except:
-			return f'Fatal error while rendering error {str(e.__context__)}.\nContact admin.', 500
-	else:
-		return page
-
-if __name__ == '__main__':
-	from gevent import pywsgi
-	server = pywsgi.WSGIServer(('127.0.0.1', 5009), app)
-	server.serve_forever()




diff --git a/static/img/avatar.svg b/static/img/avatar.svg
new file mode 100644
index 0000000000000000000000000000000000000000..a169ddac038ce7ce8cda194c2b83cc8de912bd36
--- /dev/null
+++ b/static/img/avatar.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="126.67" height="126.67" version="1.1" viewBox="0 0 126.67 126.67" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+ <metadata>
+  <rdf:RDF>
+   <cc:Work rdf:about="">
+    <dc:format>image/svg+xml</dc:format>
+    <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+    <dc:title/>
+   </cc:Work>
+  </rdf:RDF>
+ </metadata>
+ <path d="m52.875 66.367c-6.438-6.0318-11.219-11.449-10.624-12.038 0.59506-0.58892 3.4819-1.2629 6.4153-1.4977l5.3333-0.42693 0.84896-14.823c0.46693-8.1525 1.5184-17.698 2.3367-21.213 0.81826-3.5148 1.3362-7.1531 1.151-8.0851-0.23731-1.1944 1.7938-1.6946 6.8801-1.6946 6.7051 0 7.2445 0.23887 7.6068 3.3688 0.21445 1.8528 1.2705 11.689 2.3468 21.857 1.0763 10.169 2.3643 19.155 2.8622 19.969 0.49794 0.81425 3.2187 1.3116 6.0462 1.1053 2.8275-0.20632 5.5849 0.07358 6.1275 0.62199 1.3209 1.335-20.913 23.822-23.553 23.822-1.1399 0-7.34-4.935-13.778-10.966z" fill="#96bee4" stroke-width="1.3404"/>
+ <path d="m0 63.333v-63.333h126.67v126.67h-126.67zm76.919 51.248c8.4782-7.1626 14.605-14.118 17.834-20.248 1.4489-2.75 3.1879-5 3.8645-5 0.67654 0 4.5643-4.2613 8.6396-9.4696 6.0361-7.7144 7.4095-10.411 7.4095-14.548 0-4.0341-0.84366-5.7884-4.1023-8.5304-2.2563-1.8985-4.9563-3.4519-6-3.4519-1.4059 0-1.8977-1.8565-1.8977-7.1638 0-11.588-8.9008-28.638-17.622-33.756-6.5374-3.8365-16.319-6.1755-22.78-5.4473-8.3939 0.94612-21.72 7.3942-26.476 12.811-5.727 6.5227-9.0581 14.444-9.0928 21.622-0.03257 6.7476-1.7357 9.2667-6.2652 9.2667-3.6271 0-6.4625 4.0886-7.692 11.092-0.6856 3.905-0.24814 6.1862 1.8771 9.7882 4.0132 6.8019 11.125 15.068 12.989 15.096 0.88235 0.0132 2.8631 2.874 4.4016 6.3574 3.3416 7.5656 8.9523 13.68 17.994 19.61 14.54 9.5354 17.692 9.7663 26.919 1.9714z" fill="#da9d3c" stroke-width="1.3333"/>
+</svg>




diff --git a/static/style/style.css b/static/style/style.css
index f5a6f83d9d6485ba4cb4f70fd6cd2ec92d31131e..316e2228b167db51a32b5e07c901d2351b1dafdf 100644
--- a/static/style/style.css
+++ b/static/style/style.css
@@ -75,6 +75,7 @@ 	--text: #000000;
 	--bg: #ffffff;
 	--black: #121212;
 	--grey: #888888;
+	--dark-grey: #444444;
 	--unimportant: #ffffffb2;
 	--error: #892b30;
 }
@@ -89,9 +90,14 @@ 		--text: #ffffff;
 		--bg: #121212;
 		--black: #121212;
 		--grey: #888888;
+		--dark-grey: #444444;
 		--unimportant: #ffffffb2;
 		--error: #892b30;
 	}
+}
+
+input {
+	-webkit-appearance: none;
 }
 
 nav ul {
@@ -101,15 +107,15 @@ nav input:checked ~ ul {
 	display: block;
 }
 
-.watched-box, .watched-box-flex {
+.experienced-box, .experienced-box-flex {
 	display: none;
 }
 
-.watched-datetime-check:checked ~ .watched-box {
+.experienced-datetime-check:checked ~ .experienced-box {
 	display: block;
 }
 
-.watched-datetime-check:checked ~ .watched-box-flex {
+.experienced-datetime-check:checked ~ .experienced-box-flex {
 	display: flex;
 }
 
@@ -206,6 +212,10 @@
 /* TEXT COLOUR */
 .text {
 	color: var(--text);
+}
+
+.text-error {
+	color: var(--error);
 }
 
 .text-unimportant {
@@ -475,6 +485,10 @@ .bg-grey {
 	background-color: var(--grey);
 }
 
+.bg-dark-grey {
+	background-color: var(--dark-grey);
+}
+
 .hover-bg-grey {
 	outline: 0;
 	background-color: transparent;
@@ -534,12 +548,23 @@ }
 
 /* FILTER */
 
+p.spoiler {
+	color: var(--bg);
+	border: 1px solid var(--text);
+	filter: none;
+}
+
+p.spoiler:hover {
+	color: var(--text);
+	border: 1px solid transparent;
+}
+
 .spoiler {
 	filter: blur(10px);
 }
 
-.spoler:hover {
-	filter: blur(0) !important;
+.spoiler:hover {
+	filter: blur(0px);
 }
 
 .bw {
@@ -747,11 +772,11 @@ 	outline: 0;
 }
 
 .no-outline:hover, .no-outline:focus {
-	border: solid .5px var(--grey);
+	border-image: none;
+	border: solid .5px var(--grey) !important;
 }
 
 .no-outline:active{
-	outline: 0;
 	border: solid .5px var(--accent);
 }
 
@@ -783,6 +808,18 @@ }
 
 .height-all {
 	height: 100vh;
+}
+
+.min-height-231px {
+	min-height: 14.4375rem;
+}
+
+.min-height-139px {
+	min-height: 8.6875rem;
+}
+
+.min-height-104px {
+	min-height: 6.5rem;
 }
 
 /* FONT STYLE */




diff --git a/templates/about.html b/templates/about.html
index 0bbda468b00d0ff5f4479384505817f4b1145b0a..468eb7b438fc39590e547f86b7c32d5a0c0cfa1f 100644
--- a/templates/about.html
+++ b/templates/about.html
@@ -28,7 +28,7 @@ 					
    <!--<li><a href="/users/{{.State.User.Username}}" class="decoration-none text-accent">{{.Strings.Global.account}}</a><span class="material-icon padding-lr-_5">&#xe851;</span></li>--> <li><a href="/users/{{.State.User.Username}}/watchlist" class="decoration-none text-accent">{{.Strings.Global.watchlist}}</a><span class="material-icon padding-lr-_5">&#xe04a;</span></li> <li><a href="/users/{{.State.User.Username}}/tvqueue" class="decoration-none text-accent">{{.Strings.Global.tv_queue}}</a><span class="material-icon padding-lr-_5">&#xe1b2;</span></li> - <!--<li><a href="/users/{{.State.User.Username}}/readlist" class="decoration-none text-accent">{{.Strings.Global.readlist}}</a><span class="material-icon padding-lr-_5">&#xe431;</span></li>--> + <li><a href="/users/{{.State.User.Username}}/readlist" class="decoration-none text-accent">{{.Strings.Global.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">{{.Strings.Global.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"> diff --git a/templates/book.html b/templates/book.html index a447848ae017aebffd852ffb62a2480d0f47a42a..2898411271b8a2a729f39eb3fe4fc498e3cb99ef 100644 --- a/templates/book.html +++ b/templates/book.html @@ -31,7 +31,7 @@
      <!--<li><a href="/users/{{.State.User.Username}}" class="decoration-none text-accent">{{.Strings.Global.account}}</a><span class="material-icon padding-lr-_5">&#xe851;</span></li>--> <li><a href="/users/{{.State.User.Username}}/watchlist" class="decoration-none text-accent">{{.Strings.Global.watchlist}}</a><span class="material-icon padding-lr-_5">&#xe04a;</span></li> <li><a href="/users/{{.State.User.Username}}/tvqueue" class="decoration-none text-accent">{{.Strings.Global.tv_queue}}</a><span class="material-icon padding-lr-_5">&#xe1b2;</span></li> - <!--<li><a href="/users/{{.State.User.Username}}/readlist" class="decoration-none text-accent">{{.Strings.Global.readlist}}</a><span class="material-icon padding-lr-_5">&#xe431;</span></li>--> + <li><a href="/users/{{.State.User.Username}}/readlist" class="decoration-none text-accent">{{.Strings.Global.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">{{.Strings.Global.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"> @@ -56,9 +56,9 @@
<main class="margin-top-20 margin-lr-5 flex flex-row flex-wrap flex-justify-space flex-align-start margin-bottom-2"> <div class="desktop-w3 phone-w12 margin-bottom-2"> {{if .Data.Cover}} - <img src="{{.Data.Cover}}" class="block width-154px"> + <img src="{{.Data.Cover}}" class="block min-height-231px width-154px"> {{else}} - <img src="/static/img/book_empty.webp" class="block width-154px" /> + <img src="/static/img/book_empty.webp" class="block min-height-231px width-154px" /> {{end}} <div class="on-phone"> <span class="sans font-3">{{.Data.Title}}</span> @@ -78,8 +78,42 @@

{{.Data.SerieName}}{{if .Data.PartInSerie}}, {{.Strings.Book.part}} {{.Data.PartInSerie}}{{end}}

{{end}} <p class="sans text-grey margin-top-1 margin-bottom-_5"><span class="material-icon">&#xe157;</span> {{$.Strings.Book.source}}</p> <p class="margin-lr-1 sans margin-tb-_5">{{range .Data.Source}}<a href="{{.Url}}">{{.Name}}</a><br/>{{end}}</p> + {{if and (.Data.IsOnWantList) (not .State.User.IsEmpty)}} + <p class="sans text-grey margin-top-1 margin-bottom-_5"><span class="material-icon">&#xe05f;</span> {{.Strings.Book.Readlist}}</p> + <p class="margin-lr-1 sans margin-tb-_5">{{.Strings.Book.onReadlist}}</p> + {{end}} + {{if .Data.Experiences}} + <p class="sans text-grey margin-top-1 margin-bottom-_5"><span class="material-icon">&#xe431;</span> {{.Strings.Book.read}}</p> + <p class="margin-lr-1 sans margin-tb-_5">{{len .Data.Experiences}} times<br/> + last time <span title="{{.Data.GetLastExperienceFull .Strings}}">{{.Data.GetLastExperience .Strings .State.User.Timezone}}</p> + {{end}} </div> <div class="desktop-w6 phone-w12 margin-top-10 padding-lr-2 margin-bottom-2"> + <div> + {{if not .State.User.IsEmpty}} + <div class="flex flex-row flex-wrap flex-centre flex-align-centre"> + {{if and (not .Data.IsOnWantList) (not .Data.Experiences)}} + <form action="/users/{{.State.User.Username}}/readlist/" method="POST" class="margin-tb-_5 margin-lr-1"> + <input type="hidden" name="itemId" value="{{.Data.Id}}" /> + <input type="hidden" name="itemType" value="book" /> + <button type="submit" class="border-text hover-bg-grey padding-tb-_25 cursor-hand text font-2"><span class="padding-lr-_5 material-icon font-2">&#xe03b;</span><span class="sans padding-lr-_5">{{.Strings.Book.want_read}}</span></button> + </form> + {{end}} + <form action="/users/{{.State.User.Username}}/experiences/" method="POST" class="margin-tb-_5 margin-lr-1"> + <input type="hidden" name="itemId" value="{{.Data.Id}}" /> + <input type="hidden" name="itemType" value="book" /> + <button type="submit" class="border-text hover-bg-dark-accent padding-tb-_25 cursor-hand text-black font-2"><span class="padding-lr-_5 material-icon font-2">&#xe431;</span><span class="sans padding-lr-_5">{{.Strings.Book.read}}</span></button><label for="read-datetime-check" class="cursor-hand bg-accent inline-block font-2 relative top-m_3 height-3_3 text-black"> + <span class="material-icon">&#xe5cf;</span> + </label> + <input type="checkbox" id="read-datetime-check" class="display-none experienced-datetime-check" name="isOtherTime" value="true"/> + <div class="experienced-box absolute"> + <input type="date" name="experiencedDate" class="margin-lr-_5 margin-tb-_5 text bg border-text" placeholder="YYYY-mm-dd"/> + <input type="time" name="experiencedTime" class="margin-lr-_5 margin-tb-_5 text bg border-text" placeholder="HH:MM" /> + </div> + </form> + </div> + {{end}} + </div> <p>{{.Data.Description}}</p> <hr class="material-icon text-grey hr-book"/> </div> diff --git a/templates/bookserie.html b/templates/bookserie.html index 9bc78b5024735089dd2a6336d68ba9f7871b96d8..e2f6674047a31e4b6fbcf0d35926f4e31138ec24 100644 --- a/templates/bookserie.html +++ b/templates/bookserie.html @@ -31,7 +31,7 @@
<main class="margin-top-20 margin-lr-5 flex flex-row flex-wrap flex-justify-space flex-align-start margin-bottom-2"> <div class="desktop-w3 phone-w12 margin-bottom-2"> {{if .Data.Cover}} - <img src="{{.Data.Cover}}" class="block width-154px"> + <img src="{{.Data.Cover}}" class="min-height-231px block width-154px"> {{else}} - <img src="/static/img/bookserie_empty.webp" class="block width-154px" /> + <img src="/static/img/bookserie_empty.webp" class="min-height-231px block width-154px" /> {{end}} <div class="on-phone"> <span class="sans font-3">{{.Data.Title}}</span> @@ -80,9 +80,9 @@ <div class="flex"> <div> {{if .Cover}} - <img src="{{.Cover}}" class="width-92px" /> + <img src="{{.Cover}}" class="min-height-139px width-92px" /> {{else}} - <img src="/static/img/book_empty.webp" class="width-92px" /> + <img src="/static/img/book_empty.webp" class="min-height-139px width-92px" /> {{end}} </div> <div class="margin-lr-1"> diff --git a/templates/experiences.html b/templates/experiences.html index bda1abf22ed2e1f4c8dcbf026d532bad93a61142..57031cdea75772cd463e0a0dc0df51a4e021fc89 100644 --- a/templates/experiences.html +++ b/templates/experiences.html @@ -23,7 +23,7 @@