anki

package module
v0.0.0-...-03460c5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 16, 2019 License: AGPL-3.0 Imports: 14 Imported by: 0

README

Build Status GoDoc

A library to read Anki *.apkg packages in Go and GopherJS, licensed under the AGPLv3.

Documentation

Overview

Package anki provides a library to read *.apkg files produced by Anki (http://ankisrs.net/).

A *.apkg file is simply a zip-compressed archive, which contains a the following files:

  • collection.anki2 -- An SQLite3 database
  • media -- A JSON blob mapping media file filenames to numbers
  • [files numbered 0..n] -- Media files (referenced in the media file above)

The SQLite3 Database contains the following tables. Detailed explanations of each column's use can be found inline below, in the struct definitions.

CREATE TABLE col (
	id              integer primary key,
	crt             integer not null,
	mod             integer not null,
	scm             integer not null,
	ver             integer not null,
	dty             integer not null,
	usn             integer not null,
	ls              integer not null,
	conf            text not null,
	models          text not null,
	decks           text not null,
	dconf           text not null,
	tags            text not null
);

CREATE TABLE notes (
	id              integer primary key,   /* 0 */
	guid            text not null,         /* 1 */
	mid             integer not null,      /* 2 */
	mod             integer not null,      /* 3 */
	usn             integer not null,      /* 4 */
	tags            text not null,         /* 5 */
	flds            text not null,         /* 6 */
	sfld            integer not null,      /* 7 */
	csum            integer not null,      /* 8 */
	flags           integer not null,      /* 9 */
	data            text not null          /* 10 */
);

CREATE TABLE cards (
	id              integer primary key,   /* 0 */
	nid             integer not null,      /* 1 */
	did             integer not null,      /* 2 */
	ord             integer not null,      /* 3 */
	mod             integer not null,      /* 4 */
	usn             integer not null,      /* 5 */
	type            integer not null,      /* 6 */
	queue           integer not null,      /* 7 */
	due             integer not null,      /* 8 */
	ivl             integer not null,      /* 9 */
	factor          integer not null,      /* 10 */
	reps            integer not null,      /* 11 */
	lapses          integer not null,      /* 12 */
	left            integer not null,      /* 13 */
	odue            integer not null,      /* 14 */
	odid            integer not null,      /* 15 */
	flags           integer not null,      /* 16 */
	data            text not null          /* 17 */
);

CREATE TABLE graves (
	usn             integer not null,
	oid             integer not null,
	type            integer not null
);

CREATE TABLE revlog (
	id              integer primary key,
	cid             integer not null,
	usn             integer not null,
	ease            integer not null,
	ivl             integer not null,
	lastIvl         integer not null,
	factor          integer not null,
	time            integer not null,
	type            integer not null
);

When it is obvious that a column is no longer used by Anki, it is ommitted from these Go data structures. When it is not obvious, it is included, but typically with a comment to the effect that its use is unknown. If you know of any inaccuracies or recent changes to the Anki schema, please create an issue.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Apkg

type Apkg struct {
	// contains filtered or unexported fields
}

Apkg manages state of an Anki package file during processing.

func ReadBytes

func ReadBytes(b []byte) (*Apkg, error)

ReadBytes reads an *.apkg file from a bytestring, returning an Apkg struct for processing.

func ReadFile

func ReadFile(f string) (*Apkg, error)

ReadFile reads an *.apkg file, returning an Apkg struct for processing.

func ReadReader

func ReadReader(r io.ReaderAt, size int64) (*Apkg, error)

ReadReader reads an *.apkg file from an io.Reader, returning an Apkg struct for processing.

func (*Apkg) Cards

func (a *Apkg) Cards() (*Cards, error)

Cards returns a Cards struct represeting all of the non-deleted cards in the *.apkg package file.

func (*Apkg) Close

func (a *Apkg) Close() (e error)

Close closes any opened resources (io.Reader, SQLite handles, etc). Any subsequent calls to extant objects (Collection, Decks, Notes, etc) which depend on these resources may fail. Only call this method after you're completely done reading the Apkg file.

func (*Apkg) Collection

func (a *Apkg) Collection() (*Collection, error)

func (*Apkg) ListFiles

func (a *Apkg) ListFiles() []string

ListFiles returns a list of all media files in the archive.

func (*Apkg) Notes

func (a *Apkg) Notes() (*Notes, error)

Notes returns a Notes struct representing all of the Note rows in the *.apkg package file.

func (*Apkg) ReadMediaFile

func (a *Apkg) ReadMediaFile(name string) ([]byte, error)

func (*Apkg) Reviews

func (a *Apkg) Reviews() (*Reviews, error)

Reviews returns a Reviews struct representing all of the reviews of non-deleted cards in the *.apkg package file, in reverse chronological order (newest first).

type BoolInt

type BoolInt bool

BoolInt represents a boolean value stored as an int

func (*BoolInt) Scan

func (b *BoolInt) Scan(src interface{}) error

Scan implements the sql.Scanner interface for the BoolInt type.

func (*BoolInt) UnmarshalJSON

func (b *BoolInt) UnmarshalJSON(src []byte) error

UnmarshalJSON implements the json.Unmarshaler interface for the BoolInt type.

type Card

type Card struct {
	ID             ID                `db:"id"`     // Primary key
	NoteID         ID                `db:"nid"`    // Foreign Key to a Note
	DeckID         ID                `db:"did"`    // Foreign key to a Deck
	TemplateID     int               `db:"ord"`    // The Template ID, within the Model, to which this card corresponds.
	Modified       *TimestampSeconds `db:"mod"`    // Last modified time
	UpdateSequence int               `db:"usn"`    // Update sequence number
	Type           CardType          `db:"type"`   // Card type: new, learning, due
	Queue          CardQueue         `db:"queue"`  // Queue: suspended, user buried, sched buried
	Due            *TimestampSeconds `db:"due"`    // Time when the card is next due
	Interval       *DurationSeconds  `db:"ivl"`    // SRS interval in seconds
	Factor         float32           `db:"factor"` // SRS factor
	ReviewCount    int               `db:"reps"`   // Number of reviews
	Lapses         int               `db:"lapses"` // Number of times card went from "answered correctly" to "answered incorrectly" state
	Left           int               `db:"left"`   // Reviews remaining until graduation
	OriginalDue    *TimestampSeconds `db:"odue"`   // Original due time. Only used when card is in filtered deck.
	OriginalDeckID ID                `db:"odid"`   // Original Deck ID. Only used when card is in filtered deck.
}

Card definition

This definition excludes the `flags` and `data` fields, which are no longer used. Additionally, this definition modifies the original senses of `due`, `odue`, and `ivl` by converting them to a consistent representation. `Specifically

`due` and `odue` are stored in one of three states:

  • For card queue 0 (new), the due time is ignored. Here we convert it to 0.
  • For card queue 1 (learning), the due time is stored as seconds since epoch. We leave this as-is.
  • For card queue 2 (due), the due time is stored as days since the collection was created. We convert this to seconds since epoch.

`ivl` is stored either as negative seconds, or as positive days. We convert both to positive seconds.

func (*Card) Created

func (c *Card) Created() *TimestampMilliseconds

Returns the cards's creation timestamp (based on its ID)

type CardConstraint

type CardConstraint struct {
	Index     int    // Card index
	MatchType string // "any" or "all"
	Fields    []int  // Array of fields which must exist
}

A card constraint defines which fields are necessary for a particular card type to be generated. This is (apparently) auto-calculated whenever a note is created or modified.

func (*CardConstraint) UnmarshalJSON

func (c *CardConstraint) UnmarshalJSON(src []byte) error

UnmarshalJSON implements the json.Unmarshaler interface for the CardConstraint type

type CardQueue

type CardQueue int
const (
	CardQueueSchedBuried CardQueue = -3 // Sched Buried (??, possibly unused)
	CardQueueBuried      CardQueue = -2 // Buried
	CardQueueSuspended   CardQueue = -1 // Suspended
	CardQueueNew         CardQueue = 0  // New/Cram
	CardQueueLearning    CardQueue = 1  // Learning
	CardQueueReview      CardQueue = 2  // Review
	CardQueueRelearning  CardQueue = 3  // Day Learn (Relearn?)
)

CardQueue specifies the card's queue type

See https://github.com/dae/anki/blob/master/anki/sched.py#L17 and https://github.com/dae/anki/blob/master/anki/cards.py#L14

type CardType

type CardType int
const (
	CardTypeNew CardType = iota
	CardTypeLearning
	CardTypeReview
)

type Cards

type Cards struct {
	*sqlx.Rows
}

Cards is a wrapper around sqlx.Rows, which means that any standard sqlx.Rows or sql.Rows methods may be called on it. Generally, you should only ever need to call Next() and Close(), in addition to Card() which is defined in this package.

func (*Cards) Card

func (c *Cards) Card() (*Card, error)

type Collection

type Collection struct {
	ID             ID                     `db:"id"`     // Primary key; should always be 1, as there's only ever one collection per *.apkg file
	Created        *TimestampSeconds      `db:"crt"`    // Created timestamp (seconds)
	Modified       *TimestampMilliseconds `db:"mod"`    // Last modified timestamp (milliseconds)
	SchemaModified *TimestampMilliseconds `db:"scm"`    // Schema modification time (milliseconds)
	Version        int                    `db:"ver"`    // Version?
	Dirty          BoolInt                `db:"dty"`    // Dirty? No longer used. See https://github.com/dae/anki/blob/master/anki/collection.py#L90
	UpdateSequence int                    `db:"usn"`    // update sequence number. used to figure out diffs when syncing
	LastSync       *TimestampMilliseconds `db:"ls"`     // Last sync time (milliseconds)
	Config         Config                 `db:"conf"`   // JSON blob containing configuration options
	Models         Models                 `db:"models"` // JSON array of json objects containing the models (aka Note types)
	Decks          Decks                  `db:"decks"`  // JSON array of json objects containing decks
	DeckConfigs    DeckConfigs            `db:"dconf"`  // JSON blob containing deck configuration options
	Tags           string                 `db:"tags"`   // a cache of tags used in the collection
}

Collection is an Anki Collection, stored in the `col` table.

type Config

type Config struct {
	NextPos       int             `json:"nextPos"`
	EstimateTimes bool            `json:"estTimes"`
	ActiveDecks   []ID            `json:"activeDecks"` // Array of active decks(?)
	SortType      string          `json:"sortType"`
	TimeLimit     DurationSeconds `json:"timeLimit"`
	SortBackwards BoolInt         `json:"sortBackwards"`
	AddToCurrent  bool            `json:"addToCur"` // Add new cards to current deck(?)
	CurrentDeck   ID              `json:"curDeck"`
	NewBury       bool            `json:"newBury"`
	NewSpread     int             `json:"newSpread"`
	DueCounts     bool            `json:"dueCounts"`
	CurrentModel  ID              `json:"curModel"`
	CollapseTime  int             `json:"collapseTime"`
}

Config represents basic global configuration for the Anki client.

func (*Config) Scan

func (c *Config) Scan(src interface{}) error

Scan implements the sql.Scanner interface for the Config type.

type DB

type DB struct {
	*sqlx.DB
	// contains filtered or unexported fields
}

func OpenDB

func OpenDB(src io.Reader) (db *DB, e error)

func (*DB) Close

func (db *DB) Close() (e error)

type Deck

type Deck struct {
	ID                      ID                `json:"id"`               // Deck ID
	Name                    string            `json:"name"`             // Deck name
	Description             string            `json:"desc"`             // Deck description
	Modified                *TimestampSeconds `json:"mod"`              // Last modification time in seconds
	UpdateSequence          int               `json:"usn"`              // Update sequence number. Used in the same way as the other USN values
	Collapsed               bool              `json:"collapsed"`        // True when the deck is collapsed
	BrowserCollapsed        bool              `json:"browserCollapsed"` // True when the deck is collapsed in the browser
	ExtendedNewCardLimit    int               `json:"extendedNew"`      // Extended new card limit for custom study
	ExtendedReviewCardLimit int               `json:"extendedRev"`      // Extended review card limit for custom study
	Dynamic                 BoolInt           `json:"dyn"`              // True for a dynamic (aka filtered) deck
	ConfigID                ID                `json:"conf"`             // ID of option group from dconf in `col` table
	NewToday                [2]int            `json:"newToday"`         // two number array used somehow for custom study
	ReviewsToday            [2]int            `json:"revToday"`         // two number array used somehow for custom study
	LearnToday              [2]int            `json:"lrnToday"`         // two number array used somehow for custom study
	TimeToday               [2]int            `json:"timeToday"`        // two number array used somehow for custom study (in ms)
	Config                  *DeckConfig       `json:"-"`
}

A Deck definition

func (*Deck) Created

func (d *Deck) Created() *TimestampMilliseconds

Returns the deck's creation timestamp (based on its ID)

type DeckConfig

type DeckConfig struct {
	ID               ID                `json:"id"`       // Deck ID
	Name             string            `json:"name"`     // Deck Name
	ReplayAudio      bool              `json:"replayq"`  // When answer shown, replay both question and answer audio
	ShowTimer        BoolInt           `json:"timer"`    // Show answer timer
	MaxAnswerSeconds int               `json:"maxTaken"` // Ignore answers that take longer than this many seconds
	Modified         *TimestampSeconds `json:"mod"`      // Modified timestamp
	AutoPlay         bool              `json:"autoplay"` // Automatically play audio
	Lapses           struct {
		LeechFails      int               `json:"leechFails"`  // Leech threshold
		MinimumInterval DurationDays      `json:"minInt"`      // Minimum interval in days
		LeechAction     LeechAction       `json:"leechAction"` // Leech action: Suspend or Tag Only
		Delays          []DurationMinutes `json:"delays"`      // Steps in minutes
		NewInterval     float32           `json:"mult"`        // New Interval Multiplier
	} `json:"lapse"`
	Reviews struct {
		PerDay           int          `json:"perDay"` // Maximum reviews per day
		Fuzz             float32      `json:"fuzz"`   // Apparently not used?
		IntervalModifier float32      `json:"ivlFct"` // Interval modifier (fraction)
		MaxInterval      DurationDays `json:"maxIvl"` // Maximum interval in days
		EasyBonus        float32      `json:"ease4"`  // Easy bonus
		Bury             bool         `json:"bury"`   // Bury related reviews until next day
	} `json:"rev"`
	New struct {
		PerDay        int               `json:"perDay"`        // Maximum new cards per day
		Delays        []DurationMinutes `json:"delays"`        // Steps in minutes
		Bury          bool              `json:"bury"`          // Bury related cards until the next day
		Separate      bool              `json:"separate"`      // Unused??
		Intervals     [3]DurationDays   `json:"ints"`          // Intervals??
		InitialFactor float32           `json:"initialFactor"` // Starting Ease
		Order         NewCardOrder      `json:"order"`         // New card order: Random, or order added
	} `json:"new"`
}

Per-Deck configuration options.

Excluded from this definition is the `minSpace` field from Reviews, as it is no longer used.

type DeckConfigs

type DeckConfigs map[ID]*DeckConfig

Collection of per-deck configurations

func (*DeckConfigs) Scan

func (dc *DeckConfigs) Scan(src interface{}) error

Scan implements the sql.Scanner interface for the DeckConfigs type.

func (*DeckConfigs) UnmarshalJSON

func (dc *DeckConfigs) UnmarshalJSON(src []byte) error

UnmarshalJSON implements the json.Unmarshaler interface for the DeckConfigs type.

type Decks

type Decks map[ID]*Deck

A collection of Decks

func (*Decks) Scan

func (d *Decks) Scan(src interface{}) error

Scan implements the sql.Scanner interface for the Decks type.

func (*Decks) UnmarshalJSON

func (d *Decks) UnmarshalJSON(src []byte) error

UnmarshalJSON implements the json.Unmarshaler interface for the Decks type.

type DurationDays

type DurationDays int

DurationDays represents a duration in days.

func (*DurationDays) Scan

func (d *DurationDays) Scan(src interface{}) error

type DurationMilliseconds

type DurationMilliseconds time.Duration

DurationMilliseconds represents a time.Duration value stored as milliseconds.

func (*DurationMilliseconds) Scan

func (d *DurationMilliseconds) Scan(src interface{}) error

Scan implements the sql.Scanner interface for the DurationMilliseconds type.

type DurationMinutes

type DurationMinutes time.Duration

DurationMinutes represents a time.Duration value stored as minutes.

func (*DurationMinutes) Scan

func (d *DurationMinutes) Scan(src interface{}) error

Scan implements the sql.Scanner interface for the DurationMinutes type.

type DurationSeconds

type DurationSeconds time.Duration

DurationSeconds represents a time.Duration value stored as seconds.

func (*DurationSeconds) Scan

func (d *DurationSeconds) Scan(src interface{}) error

Scan implements the sql.Scanner interface for the DurationSeconds type.

type Field

type Field struct {
	Name     string `json:"name"`   // Field name
	Sticky   bool   `json:"sticky"` // Sticky fields retain the value that was last added when adding new notes
	RTL      bool   `json:"rtl"`    // boolean to indicate if this field uses Right-to-Left script
	Ordinal  int    `json:"ord"`    // Ordinal of the field. Goes from 0 to num fields -1.
	Font     string `json:"font"`   // Display font
	FontSize int    `json:"size"`   // Font size
}

A field of a model

Excluded from this definition is the `media` field, which appears to no longer be used.

type FieldValues

type FieldValues []string

func (*FieldValues) Scan

func (fv *FieldValues) Scan(src interface{}) error

Scan implements the sql.Scanner interface for the FieldValues type.

type ID

type ID int64

ID represents an Anki object ID (deck, card, note, etc) as an int64.

func (*ID) Scan

func (i *ID) Scan(src interface{}) error

Scan implements the sql.Scanner interface for the ID type.

func (*ID) UnmarshalJSON

func (i *ID) UnmarshalJSON(src []byte) error

UnmarshalJSON implements the json.Unmarshaler interface for the ID type.

type LeechAction

type LeechAction int

Enum of available leech actions

const (
	LeechActionSuspendCard LeechAction = iota
	LeechActoinTagOnly
)

type Model

type Model struct {
	ID             ID                `json:"id"`    // Model ID
	Name           string            `json:"name"`  // Model name
	Tags           []string          `json:"tags"`  // Anki saves the tags of the last added note to the current model
	DeckID         ID                `json:"did"`   // Deck ID of deck where cards are added by default
	Fields         []*Field          `json:"flds"`  // Array of Field objects
	SortField      int               `json:"sortf"` // Integer specifying which field is used for sorting in the browser
	Templates      []*Template       `json:"tmpls"`
	Type           ModelType         `json:"type"`      // Model type: Standard or Cloze
	LatexPre       string            `json:"latexPre"`  // preamble for LaTeX expressions
	LatexPost      string            `json:"latexPost"` // String added to end of LaTeX expressions (usually \\end{document})
	CSS            string            `json:"css"`       // CSS, shared for all templates
	Modified       *TimestampSeconds `json:"mod"`       // Modification time in seconds
	RequiredFields []*CardConstraint `json:"req"`       // Array of card constraints describing which fields are required for each card to be generated
	UpdateSequence int               `json:"usn"`       // Update sequence number: used in same way as other usn vales in db
}

Model (aka Note Type)

Excluded from this definition is the `vers` field, which is no longer used by Anki.

func (*Model) Created

func (m *Model) Created() *TimestampMilliseconds

Returns the model's creation timestamp (based on its ID)

type ModelType

type ModelType int

Enum representing the available Note Type Types (confusing, eh?)

const (
	// ModelTypeStandard indicates an Anki Basic note type
	ModelTypeStandard ModelType = iota
	// ModelTypeCloze indicates an Anki Cloze note type
	ModelTypeCloze
)

type Models

type Models map[ID]*Model

Models is a collection of Models (aka note types), stored as JSON in the `models` column of the `col` table.

func (*Models) Scan

func (m *Models) Scan(src interface{}) error

Scan implements the sql.Scanner interface for the Models type.

func (*Models) UnmarshalJSON

func (m *Models) UnmarshalJSON(src []byte) error

UnmarshalJSON implements the json.Unmarshaler interface for the Models type.

type NewCardOrder

type NewCardOrder int

Enum of new card order options

const (
	NewCardOrderOrderAdded NewCardOrder = iota
	NewCardOrderRandomOrder
)

type Note

type Note struct {
	ID             ID                `db:"id"`   // Primary key
	GUID           string            `db:"guid"` // globally unique id, almost certainly used for syncing
	ModelID        ID                `db:"mid"`  // Model ID
	Modified       *TimestampSeconds `db:"mod"`  // Last modified time
	UpdateSequence int               `db:"usn"`  // Update sequence number (no longer used?)
	Tags           string            `db:"tags"` // List of the note's tags
	FieldValues    FieldValues       `db:"flds"` // Values for the note's fields
	UniqueField    string            `db:"sfld"` // The text of the first field, used for Anki's simplistic uniqueness checking
	Checksum       int64             `db:"csum"` // Field checksum used for duplicate check. Integer representation of first 8 digits of sha1 hash of the first field
}

Note definition

Excludes the `flags` and `data` columns, which are no longer used

func (*Note) Created

func (n *Note) Created() *TimestampMilliseconds

Returns the notes's creation timestamp (based on its ID)

type Notes

type Notes struct {
	*sqlx.Rows
}

Notes is a wrapper around sqlx.Rows, which means that any standard sqlx.Rows or sql.Rows methods may be called on it. Generally, you should only ever need to call Next() and Close(), in addition to Note() which is defined in this package.

func (*Notes) Note

func (n *Notes) Note() (*Note, error)

Note is a simple wrapper around sqlx's StructScan(), which returns a Note struct populated from the database.

type Review

type Review struct {
	Timestamp      *TimestampSeconds    `db:"id"`      // Times when the review was done
	CardID         ID                   `db:"cid"`     // Foreign key to a Card
	UpdateSequence int                  `db:"usn"`     // Update sequence number
	Ease           ReviewEase           `db:"ease"`    // Button pushed to score recall: wrong, hard, ok, easy
	Interval       DurationSeconds      `db:"ivl"`     // SRS interval in seconds
	LastInterval   DurationSeconds      `db:"lastIvl"` // Prevoius SRS interval in seconds
	Factor         float32              `db:"factor"`  // SRS factor
	ReviewTime     DurationMilliseconds `db:"time"`    // Time spent on the review
	Type           ReviewType           `db:"type"`    // Review type: learn, review, relearn, cram
}

Review definition

`ivl` is stored either as negative seconds, or as positive days. We convert both to positive seconds.

type ReviewEase

type ReviewEase int
const (
	ReviewEaseWrong ReviewEase = 1
	ReviewEaseHard  ReviewEase = 2
	ReviewEaseOK    ReviewEase = 3
	ReviewEaseEasy  ReviewEase = 4
)

type ReviewType

type ReviewType int
const (
	ReviewTypeLearn ReviewType = iota
	ReviewTypeReview
	ReviewTypeRelearn
	ReviewTypeCram
)

type Reviews

type Reviews struct {
	*sqlx.Rows
}

func (*Reviews) Review

func (r *Reviews) Review() (*Review, error)

type Tags

type Tags []string

The Tags type represents an array of tags for a note.

func (*Tags) Scan

func (t *Tags) Scan(src interface{}) error

Scan implements the sql.Scanner interface for the Tags type.

type Template

type Template struct {
	Name                  string `json:"name"`  // Template name
	Ordinal               int    `json:"ord"`   // Template number
	QuestionFormat        string `json:"qfmt"`  // Question format
	AnswerFormat          string `json:"afmt"`  // Answer format
	BrowserQuestionFormat string `json:"bqfmt"` // Browser question format
	BrowserAnswerFormat   string `json:"bafmt"` // Browser answer format
	DeckOverride          ID     `json:"did"`   // Deck override (null by default) (??)
}

A Template definition. A template definition represents a single card type, and is stored as part of a Model.

type TimestampMilliseconds

type TimestampMilliseconds time.Time

TimestampMilliseconds represents a time.Time value stored as milliseconds.

func (*TimestampMilliseconds) Scan

func (t *TimestampMilliseconds) Scan(src interface{}) error

Scan implements the sql.Scanner interface for the TimestampMilliseconds type.

type TimestampSeconds

type TimestampSeconds time.Time

TimestampSeconds represents a time.Time value stored as seconds.

func (*TimestampSeconds) Scan

func (t *TimestampSeconds) Scan(src interface{}) error

Scan implements the sql.Scanner interface for the TimestampSeconds type.

func (*TimestampSeconds) UnmarshalJSON

func (t *TimestampSeconds) UnmarshalJSON(src []byte) error

UnmarshalJSON implements the json.Unmarshaler interface for the TimestampSeconds type.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL