radarr

package
v0.12.1 Latest Latest
Warning

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

Go to latest
Published: Jan 2, 2022 License: MIT Imports: 8 Imported by: 20

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type AddMovieInput

type AddMovieInput struct {
	Title               string           `json:"title,omitempty"`
	TitleSlug           string           `json:"titleSlug,omitempty"`
	MinimumAvailability string           `json:"minimumAvailability,omitempty"`
	RootFolderPath      string           `json:"rootFolderPath"`
	TmdbID              int64            `json:"tmdbId"`
	QualityProfileID    int64            `json:"qualityProfileId"`
	ProfileID           int64            `json:"profileId,omitempty"`
	Year                int              `json:"year,omitempty"`
	Images              []*starr.Image   `json:"images,omitempty"`
	AddOptions          *AddMovieOptions `json:"addOptions"`
	Tags                []int            `json:"tags,omitempty"`
	Monitored           bool             `json:"monitored"`
}

AddMovieInput is the input for a new movie.

type AddMovieOptions

type AddMovieOptions struct {
	SearchForMovie bool `json:"searchForMovie"`
}

AddMovieOptions are the options for finding a new movie.

type AddMovieOutput

type AddMovieOutput struct {
	ID                    int64               `json:"id"`
	Title                 string              `json:"title"`
	OriginalTitle         string              `json:"originalTitle"`
	AlternateTitles       []*AlternativeTitle `json:"alternateTitles"`
	SecondaryYearSourceID int64               `json:"secondaryYearSourceId"`
	SortTitle             string              `json:"sortTitle"`
	SizeOnDisk            int                 `json:"sizeOnDisk"`
	Status                string              `json:"status"`
	Overview              string              `json:"overview"`
	InCinemas             time.Time           `json:"inCinemas"`
	DigitalRelease        time.Time           `json:"digitalRelease"`
	Images                []*starr.Image      `json:"images"`
	Website               string              `json:"website"`
	Year                  int                 `json:"year"`
	YouTubeTrailerID      string              `json:"youTubeTrailerId"`
	Studio                string              `json:"studio"`
	Path                  string              `json:"path"`
	QualityProfileID      int64               `json:"qualityProfileId"`
	MinimumAvailability   string              `json:"minimumAvailability"`
	FolderName            string              `json:"folderName"`
	Runtime               int                 `json:"runtime"`
	CleanTitle            string              `json:"cleanTitle"`
	ImdbID                string              `json:"imdbId"`
	TmdbID                int64               `json:"tmdbId"`
	TitleSlug             string              `json:"titleSlug"`
	Genres                []string            `json:"genres"`
	Tags                  []int               `json:"tags"`
	Added                 time.Time           `json:"added"`
	AddOptions            *AddMovieOptions    `json:"addOptions"`
	Ratings               *starr.Ratings      `json:"ratings"`
	HasFile               bool                `json:"hasFile"`
	Monitored             bool                `json:"monitored"`
	IsAvailable           bool                `json:"isAvailable"`
}

AddMovieOutput is the data returned when adding a movier.

type AlternativeTitle

type AlternativeTitle struct {
	MovieID    int          `json:"movieId"`
	Title      string       `json:"title"`
	SourceType string       `json:"sourceType"`
	SourceID   int          `json:"sourceId"`
	Votes      int          `json:"votes"`
	VoteCount  int          `json:"voteCount"`
	Language   *starr.Value `json:"language"`
	ID         int          `json:"id"`
}

AlternativeTitle is part of a Movie.

type Collection

type Collection struct {
	Name   string         `json:"name"`
	TmdbID int64          `json:"tmdbId"`
	Images []*starr.Image `json:"images"`
}

Collection belongs to a Movie.

type CommandRequest added in v0.9.10

type CommandRequest struct {
	Name     string  `json:"name"`
	MovieIDs []int64 `json:"movieIds,omitempty"`
}

CommandRequest goes into the /api/v3/command endpoint. This was created from the search command and may not support other commands yet.

type CommandResponse added in v0.9.10

type CommandResponse struct {
	ID                  int64                  `json:"id"`
	Name                string                 `json:"name"`
	CommandName         string                 `json:"commandName"`
	Message             string                 `json:"message,omitempty"`
	Priority            string                 `json:"priority"`
	Status              string                 `json:"status"`
	Queued              time.Time              `json:"queued"`
	Started             time.Time              `json:"started,omitempty"`
	Ended               time.Time              `json:"ended,omitempty"`
	StateChangeTime     time.Time              `json:"stateChangeTime,omitempty"`
	LastExecutionTime   time.Time              `json:"lastExecutionTime,omitempty"`
	Duration            string                 `json:"duration,omitempty"`
	Trigger             string                 `json:"trigger"`
	SendUpdatesToClient bool                   `json:"sendUpdatesToClient"`
	UpdateScheduledTask bool                   `json:"updateScheduledTask"`
	Body                map[string]interface{} `json:"body"`
}

CommandResponse comes from the /api/v3/command endpoint.

type CustomFormat added in v0.9.10

type CustomFormat struct {
	ID                    int                 `json:"id"`
	Name                  string              `json:"name"`
	IncludeCFWhenRenaming bool                `json:"includeCustomFormatWhenRenaming"`
	Specifications        []*CustomFormatSpec `json:"specifications"`
}

CustomFormat is the api/customformat endpoint payload.

type CustomFormatField added in v0.9.10

type CustomFormatField struct {
	Order    int         `json:"order"`
	Name     string      `json:"name"`
	Label    string      `json:"label"`
	Value    interface{} `json:"value"` // should be a string, but sometimes it's a number.
	Type     string      `json:"type"`
	Advanced bool        `json:"advanced"`
}

CustomFormatField is part of a CustomFormat Specification.

type CustomFormatSpec added in v0.9.10

type CustomFormatSpec struct {
	Name               string               `json:"name"`
	Implementation     string               `json:"implementation"`
	Implementationname string               `json:"implementationName"`
	Infolink           string               `json:"infoLink"`
	Negate             bool                 `json:"negate"`
	Required           bool                 `json:"required"`
	Fields             []*CustomFormatField `json:"fields"`
}

CustomFormatSpec is part of a CustomFormat.

type Exclusion added in v0.9.10

type Exclusion struct {
	TMDBID int64  `json:"tmdbId"`
	Title  string `json:"movieTitle"`
	Year   int    `json:"movieYear"`
	ID     int64  `json:"id,omitempty"`
}

Exclusion is a Radarr excluded item.

type Field added in v0.11.11

type Field struct {
	Name          string          `json:"name"`
	Value         interface{}     `json:"value"` // sometimes number, sometimes string. 'Type' may tell you.
	Label         string          `json:"label"`
	HelpText      string          `json:"helpText"`
	Type          string          `json:"type"`
	Order         int64           `json:"order"`
	Advanced      bool            `json:"advanced"`
	SelectOptions []*SelectOption `json:"selectOptions,omitempty"`
}

Field is currently only part of ImportList.

type FormatItem added in v0.9.11

type FormatItem struct {
	Format int    `json:"format"`
	Name   string `json:"name"`
	Score  int    `json:"score"`
}

FormatItem is part of a QualityProfile.

type History

type History struct {
	Page          int              `json:"page"`
	PageSize      int              `json:"pageSize"`
	SortKey       string           `json:"sortKey"`
	SortDirection string           `json:"sortDirection"`
	TotalRecords  int              `json:"totalRecords"`
	Records       []*HistoryRecord `json:"records"`
}

History is the /api/v3/history endpoint.

type HistoryRecord added in v0.10.6

type HistoryRecord struct {
	ID                  int64          `json:"id"`
	MovieID             int64          `json:"movieId"`
	SourceTitle         string         `json:"sourceTitle"`
	Languages           []*starr.Value `json:"languages"`
	Quality             *starr.Quality `json:"quality"`
	CustomFormats       []interface{}  `json:"customFormats"`
	QualityCutoffNotMet bool           `json:"qualityCutoffNotMet"`
	Date                time.Time      `json:"date"`
	DownloadID          string         `json:"downloadId"`
	EventType           string         `json:"eventType"`
	Data                struct {
		Age                string    `json:"age"`
		AgeHours           string    `json:"ageHours"`
		AgeMinutes         string    `json:"ageMinutes"`
		DownloadClient     string    `json:"downloadClient"`
		DownloadClientName string    `json:"downloadClientName"`
		DownloadURL        string    `json:"downloadUrl"`
		DroppedPath        string    `json:"droppedPath"`
		FileID             string    `json:"fileId"`
		GUID               string    `json:"guid"`
		ImportedPath       string    `json:"importedPath"`
		Indexer            string    `json:"indexer"`
		IndexerFlags       string    `json:"indexerFlags"`
		IndexerID          string    `json:"indexerId"`
		Message            string    `json:"message"`
		NzbInfoURL         string    `json:"nzbInfoUrl"`
		Protocol           string    `json:"protocol"`
		PublishedDate      time.Time `json:"publishedDate"`
		Reason             string    `json:"reason"`
		ReleaseGroup       string    `json:"releaseGroup"`
		Size               string    `json:"size"`
		TmdbID             string    `json:"tmdbId"`
		TorrentInfoHash    string    `json:"torrentInfoHash"`
	} `json:"data"`
}

HistoryRecord is part of the History data. Not all items have all Data members. Check EventType for what you need.

type ImportList added in v0.11.11

type ImportList struct {
	ID                  int64    `json:"id"`
	Name                string   `json:"name"`
	Enabled             bool     `json:"enabled"`
	EnableAuto          bool     `json:"enableAuto"`
	ShouldMonitor       bool     `json:"shouldMonitor"`
	SearchOnAdd         bool     `json:"searchOnAdd"`
	RootFolderPath      string   `json:"rootFolderPath"`
	QualityProfileID    int64    `json:"qualityProfileId"`
	MinimumAvailability string   `json:"minimumAvailability"`
	ListType            string   `json:"listType"`
	ListOrder           int64    `json:"listOrder"`
	Fields              []*Field `json:"fields"`
	ImplementationName  string   `json:"implementationName"`
	Implementation      string   `json:"implementation"`
	ConfigContract      string   `json:"configContract"`
	InfoLink            string   `json:"infoLink"`
	Tags                []int    `json:"tags"`
}

ImportList represents the api/v3/importlist endpoint.

type MediaInfo

type MediaInfo struct {
	AudioAdditionalFeatures string  `json:"audioAdditionalFeatures"`
	AudioBitrate            int     `json:"audioBitrate"`
	AudioChannels           float64 `json:"audioChannels"`
	AudioCodec              string  `json:"audioCodec"`
	AudioLanguages          string  `json:"audioLanguages"`
	AudioStreamCount        int     `json:"audioStreamCount"`
	VideoBitDepth           int     `json:"videoBitDepth"`
	VideoBitrate            int     `json:"videoBitrate"`
	VideoCodec              string  `json:"videoCodec"`
	VideoFps                float64 `json:"videoFps"`
	Resolution              string  `json:"resolution"`
	RunTime                 string  `json:"runTime"`
	ScanType                string  `json:"scanType"`
	Subtitles               string  `json:"subtitles"`
}

MediaInfo is part of a MovieFile.

type Movie

type Movie struct {
	ID                    int64               `json:"id"`
	Title                 string              `json:"title,omitempty"`
	Path                  string              `json:"path,omitempty"`
	MinimumAvailability   string              `json:"minimumAvailability,omitempty"`
	QualityProfileID      int64               `json:"qualityProfileId,omitempty"`
	TmdbID                int64               `json:"tmdbId,omitempty"`
	OriginalTitle         string              `json:"originalTitle,omitempty"`
	AlternateTitles       []*AlternativeTitle `json:"alternateTitles,omitempty"`
	SecondaryYearSourceID int                 `json:"secondaryYearSourceId,omitempty"`
	SortTitle             string              `json:"sortTitle,omitempty"`
	SizeOnDisk            int64               `json:"sizeOnDisk,omitempty"`
	Status                string              `json:"status,omitempty"`
	Overview              string              `json:"overview,omitempty"`
	InCinemas             time.Time           `json:"inCinemas,omitempty"`
	PhysicalRelease       time.Time           `json:"physicalRelease,omitempty"`
	DigitalRelease        time.Time           `json:"digitalRelease,omitempty"`
	Images                []*starr.Image      `json:"images,omitempty"`
	Website               string              `json:"website,omitempty"`
	Year                  int                 `json:"year,omitempty"`
	YouTubeTrailerID      string              `json:"youTubeTrailerId,omitempty"`
	Studio                string              `json:"studio,omitempty"`
	FolderName            string              `json:"folderName,omitempty"`
	Runtime               int                 `json:"runtime,omitempty"`
	CleanTitle            string              `json:"cleanTitle,omitempty"`
	ImdbID                string              `json:"imdbId,omitempty"`
	TitleSlug             string              `json:"titleSlug,omitempty"`
	Certification         string              `json:"certification,omitempty"`
	Genres                []string            `json:"genres,omitempty"`
	Tags                  []int               `json:"tags,omitempty"`
	Added                 time.Time           `json:"added,omitempty"`
	Ratings               *starr.Ratings      `json:"ratings,omitempty"`
	MovieFile             *MovieFile          `json:"movieFile,omitempty"`
	Collection            *Collection         `json:"collection,omitempty"`
	HasFile               bool                `json:"hasFile,omitempty"`
	IsAvailable           bool                `json:"isAvailable,omitempty"`
	Monitored             bool                `json:"monitored"`
}

Movie is the /api/v3/movie endpoint.

type MovieFile

type MovieFile struct {
	ID                  int64          `json:"id"`
	MovieID             int64          `json:"movieId"`
	RelativePath        string         `json:"relativePath"`
	Path                string         `json:"path"`
	Size                int64          `json:"size"`
	DateAdded           time.Time      `json:"dateAdded"`
	SceneName           string         `json:"sceneName"`
	IndexerFlags        int64          `json:"indexerFlags"`
	Quality             *starr.Quality `json:"quality"`
	MediaInfo           *MediaInfo     `json:"mediaInfo"`
	QualityCutoffNotMet bool           `json:"qualityCutoffNotMet"`
	Languages           []*starr.Value `json:"languages"`
	ReleaseGroup        string         `json:"releaseGroup"`
	Edition             string         `json:"edition"`
}

MovieFile is part of a Movie.

type QualityProfile

type QualityProfile struct {
	ID                int64            `json:"id"`
	Name              string           `json:"name"`
	UpgradeAllowed    bool             `json:"upgradeAllowed"`
	Cutoff            int64            `json:"cutoff"`
	Qualities         []*starr.Quality `json:"items"`
	MinFormatScore    int64            `json:"minFormatScore"`
	CutoffFormatScore int64            `json:"cutoffFormatScore"`
	FormatItems       []*FormatItem    `json:"formatItems,omitempty"`
	Language          *starr.Value     `json:"language"`
}

QualityProfile is applied to Movies.

type Queue

type Queue struct {
	Page          int            `json:"page"`
	PageSize      int            `json:"pageSize"`
	SortKey       string         `json:"sortKey"`
	SortDirection string         `json:"sortDirection"`
	TotalRecords  int            `json:"totalRecords"`
	Records       []*QueueRecord `json:"records"`
}

Queue is the /api/v3/queue endpoint.

type QueueRecord added in v0.10.0

type QueueRecord struct {
	MovieID                 int64                  `json:"movieId"`
	Languages               []*starr.Value         `json:"languages"`
	Quality                 *starr.Quality         `json:"quality"`
	CustomFormats           []interface{}          `json:"customFormats"` // probably []int64
	Size                    float64                `json:"size"`
	Title                   string                 `json:"title"`
	Sizeleft                float64                `json:"sizeleft"`
	Timeleft                string                 `json:"timeleft"`
	EstimatedCompletionTime time.Time              `json:"estimatedCompletionTime"`
	Status                  string                 `json:"status"`
	TrackedDownloadStatus   string                 `json:"trackedDownloadStatus"`
	TrackedDownloadState    string                 `json:"trackedDownloadState"`
	StatusMessages          []*starr.StatusMessage `json:"statusMessages"`
	DownloadID              string                 `json:"downloadId"`
	Protocol                string                 `json:"protocol"`
	DownloadClient          string                 `json:"downloadClient"`
	Indexer                 string                 `json:"indexer"`
	OutputPath              string                 `json:"outputPath"`
	ID                      int64                  `json:"id"`
	ErrorMessage            string                 `json:"errorMessage"`
}

QueueRecord is part of the activity Queue.

type Radarr

type Radarr struct {
	starr.APIer
}

Radarr contains all the methods to interact with a Radarr server.

func New

func New(config *starr.Config) *Radarr

New returns a Radarr object used to interact with the Radarr API.

func (*Radarr) AddCustomFormat added in v0.9.10

func (r *Radarr) AddCustomFormat(format *CustomFormat) (*CustomFormat, error)

AddCustomFormat creates a new custom format and returns the response (with ID).

func (*Radarr) AddExclusions added in v0.9.10

func (r *Radarr) AddExclusions(exclusions []*Exclusion) error

AddExclusions adds an exclusion to Radarr.

func (*Radarr) AddMovie

func (r *Radarr) AddMovie(movie *AddMovieInput) (*AddMovieOutput, error)

AddMovie adds a movie to the queue.

func (*Radarr) AddQualityProfile added in v0.9.11

func (r *Radarr) AddQualityProfile(profile *QualityProfile) (int64, error)

AddQualityProfile updates a quality profile in place.

func (*Radarr) AddTag

func (r *Radarr) AddTag(label string) (int, error)

AddTag adds a tag or returns the ID for an existing tag.

func (*Radarr) CreateImportList added in v0.11.11

func (r *Radarr) CreateImportList(il *ImportList) (*ImportList, error)

CreateImportList creates an import list in Radarr.

func (*Radarr) DeleteExclusions added in v0.9.10

func (r *Radarr) DeleteExclusions(ids []int64) error

DeleteExclusions removes exclusions from Radarr.

func (*Radarr) DeleteImportList added in v0.11.11

func (r *Radarr) DeleteImportList(ids []int64) error

DeleteImportList removes an import list from Radarr.

func (*Radarr) Fail added in v0.12.0

func (r *Radarr) Fail(historyID int64) error

Fail marks the given history item as failed by id.

func (*Radarr) GetBackupFiles added in v0.12.0

func (r *Radarr) GetBackupFiles() ([]*starr.BackupFile, error)

GetBackupFiles returns all available Radarr backup files. Use GetBody to download a file using BackupFile.Path.

func (*Radarr) GetCommands added in v0.9.10

func (r *Radarr) GetCommands() ([]*CommandResponse, error)

GetCommands returns all available Radarr commands.

func (*Radarr) GetCustomFormats added in v0.9.10

func (r *Radarr) GetCustomFormats() ([]*CustomFormat, error)

GetCustomFormats returns all configured Custom Formats.

func (*Radarr) GetExclusions added in v0.9.10

func (r *Radarr) GetExclusions() ([]*Exclusion, error)

GetExclusions returns all configured exclusions from Radarr.

func (*Radarr) GetHistory

func (r *Radarr) GetHistory(records, perPage int) (*History, error)

GetHistory returns the Radarr History (grabs/failures/completed). WARNING: 12/30/2021 - this method changed. The second argument no longer controls which page is returned, but instead adjusts the pagination size. If you need control over the page, use radarr.GetHistoryPage(). This function simply returns the number of history records desired, up to the number of records present in the application. It grabs records in (paginated) batches of perPage, and concatenates them into one list. Passing zero for records will return all of them.

func (*Radarr) GetHistoryPage added in v0.12.0

func (r *Radarr) GetHistoryPage(params *starr.Req) (*History, error)

GetHistoryPage returns a single page from the Radarr History (grabs/failures/completed). The page size and number is configurable with the input request parameters.

func (*Radarr) GetImportLists added in v0.11.11

func (r *Radarr) GetImportLists() ([]*ImportList, error)

GetImportLists returns all import lists.

func (*Radarr) GetMovie

func (r *Radarr) GetMovie(tmdbID int64) ([]*Movie, error)

GetMovie grabs a movie from the queue, or all movies if tmdbId is 0.

func (*Radarr) GetMovieByID

func (r *Radarr) GetMovieByID(movieID int64) (*Movie, error)

GetMovieByID grabs a movie from the database by DB [movie] ID.

func (*Radarr) GetQualityProfiles

func (r *Radarr) GetQualityProfiles() ([]*QualityProfile, error)

GetQualityProfiles returns all configured quality profiles.

func (*Radarr) GetQueue

func (r *Radarr) GetQueue(records, perPage int) (*Queue, error)

GetQueue returns a single page from the Radarr Queue (processing, but not yet imported). WARNING: 12/30/2021 - this method changed. The second argument no longer controls which page is returned, but instead adjusts the pagination size. If you need control over the page, use radarr.GetQueuePage(). This function simply returns the number of queue records desired, up to the number of records present in the application. It grabs records in (paginated) batches of perPage, and concatenates them into one list. Passing zero for records will return all of them.

func (*Radarr) GetQueuePage added in v0.12.0

func (r *Radarr) GetQueuePage(params *starr.Req) (*Queue, error)

GetQueuePage returns a single page from the Radarr Queue. The page size and number is configurable with the input request parameters.

func (*Radarr) GetRootFolders

func (r *Radarr) GetRootFolders() ([]*RootFolder, error)

GetRootFolders returns all configured root folders.

func (*Radarr) GetSystemStatus

func (r *Radarr) GetSystemStatus() (*SystemStatus, error)

GetSystemStatus returns system status.

func (*Radarr) GetTags

func (r *Radarr) GetTags() ([]*starr.Tag, error)

GetTags returns all the tags.

func (*Radarr) Lookup added in v0.12.0

func (r *Radarr) Lookup(term string) ([]*Movie, error)

Lookup will search for movies matching the specified search term.

func (*Radarr) SendCommand added in v0.9.10

func (r *Radarr) SendCommand(cmd *CommandRequest) (*CommandResponse, error)

SendCommand sends a command to Radarr.

func (*Radarr) UpdateCustomFormat added in v0.9.10

func (r *Radarr) UpdateCustomFormat(cf *CustomFormat, cfID int) (*CustomFormat, error)

UpdateCustomFormat updates an existing custom format and returns the response.

func (*Radarr) UpdateImportList added in v0.11.11

func (r *Radarr) UpdateImportList(list *ImportList) (*ImportList, error)

UpdateImportList updates an existing import list and returns the response.

func (*Radarr) UpdateMovie

func (r *Radarr) UpdateMovie(movieID int64, movie *Movie) error

UpdateMovie sends a PUT request to update a movie in place.

func (*Radarr) UpdateQualityProfile added in v0.9.11

func (r *Radarr) UpdateQualityProfile(profile *QualityProfile) error

UpdateQualityProfile updates a quality profile in place.

func (*Radarr) UpdateTag

func (r *Radarr) UpdateTag(tagID int, label string) (int, error)

UpdateTag updates the label for a tag.

type RootFolder

type RootFolder struct {
	ID              int64         `json:"id"`
	Path            string        `json:"path"`
	Accessible      bool          `json:"accessible"`
	FreeSpace       int64         `json:"freeSpace"`
	UnmappedFolders []*starr.Path `json:"unmappedFolders"`
}

RootFolder is the /rootFolder endpoint.

type SelectOption added in v0.11.11

type SelectOption struct {
	Value        int    `json:"value"`
	Name         string `json:"name"`
	Order        int    `json:"order"`
	DividerAfter bool   `json:"dividerAfter"`
}

SelectOption is part of a Field from an ImportList.

type SystemStatus

type SystemStatus struct {
	Version           string    `json:"version"`
	BuildTime         time.Time `json:"buildTime"`
	StartupPath       string    `json:"startupPath"`
	AppData           string    `json:"appData"`
	OsName            string    `json:"osName"`
	OsVersion         string    `json:"osVersion"`
	Branch            string    `json:"branch"`
	Authentication    string    `json:"authentication"`
	SqliteVersion     string    `json:"sqliteVersion"`
	URLBase           string    `json:"urlBase"`
	RuntimeVersion    string    `json:"runtimeVersion"`
	RuntimeName       string    `json:"runtimeName"`
	MigrationVersion  int       `json:"migrationVersion"`
	IsDebug           bool      `json:"isDebug"`
	IsProduction      bool      `json:"isProduction"`
	IsAdmin           bool      `json:"isAdmin"`
	IsUserInteractive bool      `json:"isUserInteractive"`
	IsNetCore         bool      `json:"isNetCore"`
	IsMono            bool      `json:"isMono"`
	IsLinux           bool      `json:"isLinux"`
	IsOsx             bool      `json:"isOsx"`
	IsWindows         bool      `json:"isWindows"`
}

SystemStatus is the /api/v3/system/status endpoint.

Jump to

Keyboard shortcuts

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