cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists ๐Ÿƒ
charm leaflet readability golang

Compare changes

Choose any two refs to compare.

+10127 -5005
+6
.vscode/settings.json
··· 1 + { 2 + "[typescript]": { "editor.defaultFormatter": "dprint.dprint" }, 3 + "[typescriptreact]": { "editor.defaultFormatter": "dprint.dprint" }, 4 + "[javascript]": { "editor.defaultFormatter": "dprint.dprint" }, 5 + "[javascriptreact]": { "editor.defaultFormatter": "dprint.dprint" } 6 + }
+50
README.md
··· 30 30 ### First Steps 31 31 32 32 For a comprehensive walkthrough including task management, time tracking, notes, and media tracking, see the [Quickstart Guide](website/docs/Quickstart.md). 33 + 34 + ## Development 35 + 36 + Noteleaf uses [Task](https://taskfile.dev) for build automation. Development builds include additional tooling commands not available in production builds. 37 + 38 + ### Building 39 + 40 + ```sh 41 + # Production build 42 + task build 43 + 44 + # Development build (with version info and dev tools) 45 + task build:dev 46 + 47 + # Run tests 48 + task test 49 + task cov # ...with coverage 50 + ``` 51 + 52 + ### Development Tools 53 + 54 + Dev builds (`task build:dev`) include a `tools` subcommand with maintenance utilities: 55 + 56 + **Documentation Generation:** 57 + 58 + ```sh 59 + # Generate Docusaurus documentation 60 + noteleaf tools docgen --format docusaurus --out website/docs/manual 61 + 62 + # Generate man pages 63 + noteleaf tools docgen --format man --out docs/manual 64 + ``` 65 + 66 + **Data Synchronization:** 67 + 68 + ```sh 69 + # Fetch Leaflet lexicons from GitHub 70 + noteleaf tools fetch lexicons 71 + 72 + # Fetch from a specific commit 73 + noteleaf tools fetch lexicons --sha abc123def 74 + 75 + # Generic GitHub repository archive fetcher 76 + noteleaf tools fetch gh-repo \ 77 + --repo owner/repo \ 78 + --path schemas/ \ 79 + --output local/schemas/ 80 + ``` 81 + 82 + Production builds (`task build:rc`, `task build:prod`) use the `-tags prod` flag to exclude dev tools.
+5 -5
Taskfile.yml
··· 51 51 - echo "Built {{.BUILD_DIR}}/{{.BINARY_NAME}}" 52 52 53 53 build:dev: 54 - desc: Build binary with dev version (includes git commit hash) 54 + desc: Build binary with dev version (includes git commit hash and dev tools) 55 55 vars: 56 56 VERSION: "{{.GIT_DESCRIBE}}" 57 57 LDFLAGS: "-X {{.VERSION_PKG}}.Version={{.VERSION}} -X {{.VERSION_PKG}}.Commit={{.GIT_COMMIT}} -X {{.VERSION_PKG}}.BuildDate={{.BUILD_DATE}}" ··· 61 61 - 'echo "Built {{.BUILD_DIR}}/{{.BINARY_NAME}} (version: {{.VERSION}})"' 62 62 63 63 build:rc: 64 - desc: Build release candidate binary (requires git tag with -rc suffix) 64 + desc: Build release candidate binary (requires git tag with -rc suffix, excludes dev tools) 65 65 vars: 66 66 VERSION: "{{.GIT_TAG}}" 67 67 LDFLAGS: "-X {{.VERSION_PKG}}.Version={{.VERSION}} -X {{.VERSION_PKG}}.Commit={{.GIT_COMMIT}} -X {{.VERSION_PKG}}.BuildDate={{.BUILD_DATE}}" ··· 72 72 msg: "Git tag must contain '-rc' for release candidate builds (e.g., v1.0.0-rc1)" 73 73 cmds: 74 74 - mkdir -p {{.BUILD_DIR}} 75 - - go build -ldflags "{{.LDFLAGS}}" -o {{.BUILD_DIR}}/{{.BINARY_NAME}} {{.CMD_DIR}} 75 + - go build -tags prod -ldflags "{{.LDFLAGS}}" -o {{.BUILD_DIR}}/{{.BINARY_NAME}} {{.CMD_DIR}} 76 76 - 'echo "Built {{.BUILD_DIR}}/{{.BINARY_NAME}} (version: {{.VERSION}})"' 77 77 78 78 build:prod: 79 - desc: Build production binary (requires clean semver git tag) 79 + desc: Build production binary (requires clean semver git tag, excludes dev tools) 80 80 vars: 81 81 VERSION: "{{.GIT_TAG}}" 82 82 LDFLAGS: "-X {{.VERSION_PKG}}.Version={{.VERSION}} -X {{.VERSION_PKG}}.Commit={{.GIT_COMMIT}} -X {{.VERSION_PKG}}.BuildDate={{.BUILD_DATE}}" ··· 89 89 msg: "Working directory must be clean (no uncommitted changes) for production builds" 90 90 cmds: 91 91 - mkdir -p {{.BUILD_DIR}} 92 - - go build -ldflags "{{.LDFLAGS}}" -o {{.BUILD_DIR}}/{{.BINARY_NAME}} {{.CMD_DIR}} 92 + - go build -tags prod -ldflags "{{.LDFLAGS}}" -o {{.BUILD_DIR}}/{{.BINARY_NAME}} {{.CMD_DIR}} 93 93 - 'echo "Built {{.BUILD_DIR}}/{{.BINARY_NAME}} (version: {{.VERSION}})"' 94 94 95 95 clean:
+9 -6
cmd/commands.go
··· 30 30 Short: "Manage movie watch queue", 31 31 Long: `Track movies you want to watch. 32 32 33 - Search TMDB for movies and add them to your queue. Mark movies as watched when 34 - completed. Maintains a history of your movie watching activity.`, 33 + Search for movies and add them to your queue. Mark movies as watched 34 + when completed. Maintains a history of your movie watching activity.`, 35 35 } 36 36 37 + // TODO: add colors 38 + // TODO: fix critic score parsing 37 39 addCmd := &cobra.Command{ 38 40 Use: "add [search query...]", 39 41 Short: "Search and add movie to watch queue", ··· 54 56 addCmd.Flags().BoolP("interactive", "i", false, "Use interactive interface for movie selection") 55 57 root.AddCommand(addCmd) 56 58 59 + // TODO: add interactive list view 57 60 root.AddCommand(&cobra.Command{ 58 61 Use: "list [--all|--watched|--queued]", 59 62 Short: "List movies in queue with status filtering", ··· 102 105 return c.handler.Remove(cmd.Context(), args[0]) 103 106 }, 104 107 }) 105 - 106 108 return root 107 109 } 108 110 ··· 122 124 Short: "Manage TV show watch queue", 123 125 Long: `Track TV shows and episodes. 124 126 125 - Search TMDB for TV shows and add them to your queue. Track which shows you're 126 - currently watching, mark episodes as watched, and maintain a complete history 127 - of your viewing activity.`, 127 + Search for TV shows and add them to your queue. Track which shows you're currently 128 + watching, mark episodes as watched, and maintain a complete history of your viewing 129 + activity.`, 128 130 } 129 131 130 132 addCmd := &cobra.Command{ ··· 147 149 addCmd.Flags().BoolP("interactive", "i", false, "Use interactive interface for TV show selection") 148 150 root.AddCommand(addCmd) 149 151 152 + // TODO: Add interactive list view 150 153 root.AddCommand(&cobra.Command{ 151 154 Use: "list [--all|--queued|--watching|--watched]", 152 155 Short: "List TV shows in queue with status filtering",
+9 -2
cmd/main.go
··· 14 14 "github.com/stormlightlabs/noteleaf/internal/ui" 15 15 "github.com/stormlightlabs/noteleaf/internal/utils" 16 16 "github.com/stormlightlabs/noteleaf/internal/version" 17 - "github.com/stormlightlabs/noteleaf/tools" 18 17 ) 19 18 20 19 var ( ··· 25 24 newBookHandler = handlers.NewBookHandler 26 25 newArticleHandler = handlers.NewArticleHandler 27 26 newPublicationHandler = handlers.NewPublicationHandler 27 + newDocumentHandler = handlers.NewDocumentHandler 28 28 exc = fang.Execute 29 29 ) 30 30 ··· 214 214 return 1 215 215 } 216 216 217 + documentHandler, err := newDocumentHandler() 218 + if err != nil { 219 + log.Error("failed to create document handler", "err", err) 220 + return 1 221 + } 222 + 217 223 root := rootCmd() 218 224 219 225 coreGroups := []CommandGroup{ ··· 221 227 NewNoteCommand(noteHandler), 222 228 NewPublicationCommand(publicationHandler), 223 229 NewArticleCommand(articleHandler), 230 + NewSearchCommand(documentHandler), 224 231 } 225 232 226 233 for _, group := range coreGroups { ··· 251 258 root.AddCommand(cmd) 252 259 } 253 260 254 - root.AddCommand(tools.NewDocGenCommand(root)) 261 + registerTools(root) 255 262 256 263 opts := []fang.Option{ 257 264 fang.WithVersion(version.String()),
+56 -13
cmd/publication_commands.go
··· 59 59 handle = args[0] 60 60 } 61 61 62 + // Auto-fill with last authenticated handle if available 63 + if handle == "" { 64 + lastHandle := c.handler.GetLastAuthenticatedHandle() 65 + if lastHandle != "" { 66 + handle = lastHandle 67 + } 68 + } 69 + 62 70 password, _ := cmd.Flags().GetString("password") 63 71 64 72 if handle != "" && password != "" { ··· 214 222 isDraft, _ := cmd.Flags().GetBool("draft") 215 223 preview, _ := cmd.Flags().GetBool("preview") 216 224 validate, _ := cmd.Flags().GetBool("validate") 225 + output, _ := cmd.Flags().GetString("output") 226 + plaintext, _ := cmd.Flags().GetBool("plaintext") 227 + txt, _ := cmd.Flags().GetBool("txt") 228 + 229 + if txt { 230 + plaintext = true 231 + } 217 232 218 233 defer c.handler.Close() 219 234 220 235 if preview { 221 - return c.handler.PostPreview(cmd.Context(), noteID, isDraft) 236 + return c.handler.PostPreview(cmd.Context(), noteID, isDraft, output, plaintext) 222 237 } 223 238 224 239 if validate { 225 - return c.handler.PostValidate(cmd.Context(), noteID, isDraft) 240 + return c.handler.PostValidate(cmd.Context(), noteID, isDraft, output, plaintext) 226 241 } 227 242 228 243 return c.handler.Post(cmd.Context(), noteID, isDraft) ··· 231 246 postCmd.Flags().Bool("draft", false, "Create as draft instead of publishing") 232 247 postCmd.Flags().Bool("preview", false, "Show what would be posted without actually posting") 233 248 postCmd.Flags().Bool("validate", false, "Validate markdown conversion without posting") 249 + postCmd.Flags().StringP("output", "o", "", "Write document to file (defaults to JSON format)") 250 + postCmd.Flags().Bool("plaintext", false, "Use plaintext format for output file") 251 + postCmd.Flags().Bool("txt", false, "Alias for --plaintext") 234 252 root.AddCommand(postCmd) 235 253 236 254 patchCmd := &cobra.Command{ ··· 257 275 258 276 preview, _ := cmd.Flags().GetBool("preview") 259 277 validate, _ := cmd.Flags().GetBool("validate") 278 + output, _ := cmd.Flags().GetString("output") 279 + plaintext, _ := cmd.Flags().GetBool("plaintext") 280 + txt, _ := cmd.Flags().GetBool("txt") 281 + 282 + if txt { 283 + plaintext = true 284 + } 260 285 261 286 defer c.handler.Close() 262 287 263 288 if preview { 264 - return c.handler.PatchPreview(cmd.Context(), noteID) 289 + return c.handler.PatchPreview(cmd.Context(), noteID, output, plaintext) 265 290 } 266 291 267 292 if validate { 268 - return c.handler.PatchValidate(cmd.Context(), noteID) 293 + return c.handler.PatchValidate(cmd.Context(), noteID, output, plaintext) 269 294 } 270 295 271 296 return c.handler.Patch(cmd.Context(), noteID) ··· 273 298 } 274 299 patchCmd.Flags().Bool("preview", false, "Show what would be updated without actually patching") 275 300 patchCmd.Flags().Bool("validate", false, "Validate markdown conversion without patching") 301 + patchCmd.Flags().StringP("output", "o", "", "Write document to file (defaults to JSON format)") 302 + patchCmd.Flags().Bool("plaintext", false, "Use plaintext format for output file") 303 + patchCmd.Flags().Bool("txt", false, "Alias for --plaintext") 276 304 root.AddCommand(patchCmd) 277 305 278 306 pushCmd := &cobra.Command{ 279 - Use: "push [note-ids...]", 307 + Use: "push [note-ids...] [--file files...]", 280 308 Short: "Create or update multiple documents on leaflet", 281 309 Long: `Batch publish or update multiple local notes to leaflet.pub. 282 310 ··· 287 315 This is useful for bulk operations and continuous publishing workflows. 288 316 289 317 Examples: 290 - noteleaf pub push 1 2 3 # Publish/update notes 1, 2, and 3 291 - noteleaf pub push 42 99 --draft # Create/update as drafts`, 292 - Args: cobra.MinimumNArgs(1), 318 + noteleaf pub push 1 2 3 # Publish/update notes 1, 2, and 3 319 + noteleaf pub push 42 99 --draft # Create/update as drafts 320 + noteleaf pub push --file article.md # Create note from file and push 321 + noteleaf pub push --file a.md b.md --draft # Create notes from multiple files 322 + noteleaf pub push 1 2 --dry-run # Validate without pushing 323 + noteleaf pub push --file article.md --dry-run # Create note but don't push`, 293 324 RunE: func(cmd *cobra.Command, args []string) error { 325 + isDraft, _ := cmd.Flags().GetBool("draft") 326 + dryRun, _ := cmd.Flags().GetBool("dry-run") 327 + files, _ := cmd.Flags().GetStringSlice("file") 328 + 329 + defer c.handler.Close() 330 + 331 + if len(files) > 0 { 332 + return c.handler.PushFromFiles(cmd.Context(), files, isDraft, dryRun) 333 + } 334 + 335 + if len(args) == 0 { 336 + return fmt.Errorf("no note IDs or files provided") 337 + } 338 + 294 339 noteIDs := make([]int64, len(args)) 295 340 for i, arg := range args { 296 341 id, err := parseNoteID(arg) ··· 300 345 noteIDs[i] = id 301 346 } 302 347 303 - isDraft, _ := cmd.Flags().GetBool("draft") 304 - 305 - defer c.handler.Close() 306 - return c.handler.Push(cmd.Context(), noteIDs, isDraft) 348 + return c.handler.Push(cmd.Context(), noteIDs, isDraft, dryRun) 307 349 }, 308 350 } 309 351 pushCmd.Flags().Bool("draft", false, "Create/update as drafts instead of publishing") 352 + pushCmd.Flags().Bool("dry-run", false, "Create note records but skip leaflet push") 353 + pushCmd.Flags().StringSliceP("file", "f", []string{}, "Create notes from markdown files before pushing") 310 354 root.AddCommand(pushCmd) 311 - 312 355 return root 313 356 } 314 357
+1 -1
cmd/publication_commands_test.go
··· 71 71 "status", 72 72 "post [note-id]", 73 73 "patch [note-id]", 74 - "push [note-ids...]", 74 + "push [note-ids...] [--file files...]", 75 75 } 76 76 77 77 for _, expected := range expectedSubcommands {
+67
cmd/search_commands.go
··· 1 + package main 2 + 3 + import ( 4 + "strings" 5 + 6 + "github.com/spf13/cobra" 7 + "github.com/stormlightlabs/noteleaf/internal/handlers" 8 + ) 9 + 10 + // SearchCommand implements [CommandGroup] for document search commands 11 + type SearchCommand struct { 12 + handler *handlers.DocumentHandler 13 + } 14 + 15 + // NewSearchCommand creates a new search command group 16 + func NewSearchCommand(handler *handlers.DocumentHandler) *SearchCommand { 17 + return &SearchCommand{handler: handler} 18 + } 19 + 20 + func (c *SearchCommand) Create() *cobra.Command { 21 + root := &cobra.Command{ 22 + Use: "search", 23 + Short: "Search notes using TF-IDF", 24 + Long: `Full-text search for notes using Term Frequency-Inverse Document Frequency (TF-IDF) ranking. 25 + 26 + The search engine tokenizes text, builds an inverted index, and ranks results by relevance. 27 + Results are sorted by TF-IDF score, with higher scores indicating better matches.`, 28 + } 29 + 30 + queryCmd := &cobra.Command{ 31 + Use: "query [search terms...]", 32 + Short: "Search for documents matching query terms", 33 + Long: `Search for documents using TF-IDF ranking. 34 + 35 + Examples: 36 + noteleaf search query go programming 37 + noteleaf search query "machine learning" --limit 5`, 38 + Args: cobra.MinimumNArgs(1), 39 + RunE: func(cmd *cobra.Command, args []string) error { 40 + query := strings.Join(args, " ") 41 + limit, _ := cmd.Flags().GetInt("limit") 42 + 43 + return c.handler.Search(cmd.Context(), query, limit) 44 + }, 45 + } 46 + queryCmd.Flags().IntP("limit", "l", 10, "Maximum number of results to return") 47 + root.AddCommand(queryCmd) 48 + 49 + rebuildCmd := &cobra.Command{ 50 + Use: "rebuild", 51 + Short: "Rebuild search index from notes", 52 + Long: `Rebuild the search index from all notes in the database. 53 + 54 + This command: 55 + 1. Clears the existing document index 56 + 2. Copies all notes to the documents table 57 + 3. Builds a new TF-IDF search index 58 + 59 + Run this after adding, updating, or deleting notes to refresh the search index.`, 60 + RunE: func(cmd *cobra.Command, args []string) error { 61 + return c.handler.RebuildIndex(cmd.Context()) 62 + }, 63 + } 64 + root.AddCommand(rebuildCmd) 65 + 66 + return root 67 + }
+244 -4
cmd/task_commands.go
··· 1 1 package main 2 2 3 3 import ( 4 + "fmt" 5 + "strconv" 4 6 "strings" 5 7 6 8 "github.com/spf13/cobra" ··· 33 35 &cobra.Group{ID: "task-ops", Title: "Basic Operations"}, 34 36 &cobra.Group{ID: "task-meta", Title: "Metadata"}, 35 37 &cobra.Group{ID: "task-tracking", Title: "Tracking"}, 38 + &cobra.Group{ID: "task-reports", Title: "Reports & Views"}, 36 39 ) 37 40 38 41 for _, init := range []func(*handlers.TaskHandler) *cobra.Command{ 39 - addTaskCmd, listTaskCmd, viewTaskCmd, updateTaskCmd, editTaskCmd, deleteTaskCmd, 42 + addTaskCmd, listTaskCmd, viewTaskCmd, updateTaskCmd, editTaskCmd, deleteTaskCmd, taskAnnotateCmd, taskBulkEditCmd, 40 43 } { 41 44 cmd := init(c.handler) 42 45 cmd.GroupID = "task-ops" ··· 52 55 } 53 56 54 57 for _, init := range []func(*handlers.TaskHandler) *cobra.Command{ 55 - timesheetViewCmd, taskStartCmd, taskStopCmd, taskCompleteCmd, taskRecurCmd, taskDependCmd, 58 + timesheetViewCmd, taskStartCmd, taskStopCmd, taskCompleteCmd, taskRecurCmd, taskDependCmd, taskUndoCmd, taskHistoryCmd, 56 59 } { 57 60 cmd := init(c.handler) 58 61 cmd.GroupID = "task-tracking" 59 62 root.AddCommand(cmd) 60 63 } 61 64 65 + for _, init := range []func(*handlers.TaskHandler) *cobra.Command{ 66 + nextActionsCmd, reportCompletedCmd, reportWaitingCmd, reportBlockedCmd, calendarCmd, 67 + } { 68 + cmd := init(c.handler) 69 + cmd.GroupID = "task-reports" 70 + root.AddCommand(cmd) 71 + } 72 + 62 73 return root 63 74 } 64 75 ··· 84 95 project, _ := c.Flags().GetString("project") 85 96 context, _ := c.Flags().GetString("context") 86 97 due, _ := c.Flags().GetString("due") 98 + wait, _ := c.Flags().GetString("wait") 99 + scheduled, _ := c.Flags().GetString("scheduled") 87 100 recur, _ := c.Flags().GetString("recur") 88 101 until, _ := c.Flags().GetString("until") 89 102 parent, _ := c.Flags().GetString("parent") ··· 91 104 tags, _ := c.Flags().GetStringSlice("tags") 92 105 93 106 defer h.Close() 94 - return h.Create(c.Context(), description, priority, project, context, due, recur, until, parent, dependsOn, tags) 107 + // TODO: Make a CreateTask struct 108 + return h.Create(c.Context(), description, priority, project, context, due, wait, scheduled, recur, until, parent, dependsOn, tags) 95 109 }, 96 110 } 97 111 addCommonTaskFlags(cmd) 98 112 addDueDateFlag(cmd) 113 + addWaitScheduledFlags(cmd) 99 114 addRecurrenceFlags(cmd) 100 115 addParentFlag(cmd) 101 116 addDependencyFlags(cmd) ··· 120 135 priority, _ := c.Flags().GetString("priority") 121 136 project, _ := c.Flags().GetString("project") 122 137 context, _ := c.Flags().GetString("context") 138 + sortBy, _ := c.Flags().GetString("sort") 123 139 124 140 defer h.Close() 125 - return h.List(c.Context(), static, showAll, status, priority, project, context) 141 + // TODO: TaskFilter struct 142 + return h.List(c.Context(), static, showAll, status, priority, project, context, sortBy) 126 143 }, 127 144 } 128 145 cmd.Flags().BoolP("interactive", "i", false, "Force interactive mode (default)") ··· 132 149 cmd.Flags().String("priority", "", "Filter by priority") 133 150 cmd.Flags().String("project", "", "Filter by project") 134 151 cmd.Flags().String("context", "", "Filter by context") 152 + cmd.Flags().String("sort", "", "Sort by (urgency)") 135 153 136 154 return cmd 137 155 } ··· 451 469 return root 452 470 } 453 471 472 + func nextActionsCmd(h *handlers.TaskHandler) *cobra.Command { 473 + cmd := &cobra.Command{ 474 + Use: "next", 475 + Short: "Show next actions (actionable tasks sorted by urgency)", 476 + Aliases: []string{"na"}, 477 + Long: `Display actionable tasks sorted by urgency score. 478 + 479 + Shows tasks that can be worked on now (not waiting, not blocked, not completed), 480 + ordered by their computed urgency based on priority, due date, age, and other factors.`, 481 + RunE: func(c *cobra.Command, args []string) error { 482 + limit, _ := c.Flags().GetInt("limit") 483 + defer h.Close() 484 + return h.NextActions(c.Context(), limit) 485 + }, 486 + } 487 + cmd.Flags().IntP("limit", "n", 10, "Limit number of tasks shown") 488 + return cmd 489 + } 490 + 491 + func reportCompletedCmd(h *handlers.TaskHandler) *cobra.Command { 492 + cmd := &cobra.Command{ 493 + Use: "completed", 494 + Short: "Show completed tasks", 495 + Long: "Display tasks that have been completed, sorted by completion date.", 496 + RunE: func(c *cobra.Command, args []string) error { 497 + limit, _ := c.Flags().GetInt("limit") 498 + defer h.Close() 499 + return h.ReportCompleted(c.Context(), limit) 500 + }, 501 + } 502 + cmd.Flags().IntP("limit", "n", 20, "Limit number of tasks shown") 503 + return cmd 504 + } 505 + 506 + func reportWaitingCmd(h *handlers.TaskHandler) *cobra.Command { 507 + cmd := &cobra.Command{ 508 + Use: "waiting", 509 + Short: "Show waiting tasks", 510 + Long: "Display tasks that are waiting for a specific date before becoming actionable.", 511 + RunE: func(c *cobra.Command, args []string) error { 512 + defer h.Close() 513 + return h.ReportWaiting(c.Context()) 514 + }, 515 + } 516 + return cmd 517 + } 518 + 519 + func reportBlockedCmd(h *handlers.TaskHandler) *cobra.Command { 520 + cmd := &cobra.Command{ 521 + Use: "blocked", 522 + Short: "Show blocked tasks", 523 + Long: "Display tasks that are blocked by dependencies on other tasks.", 524 + RunE: func(c *cobra.Command, args []string) error { 525 + defer h.Close() 526 + return h.ReportBlocked(c.Context()) 527 + }, 528 + } 529 + return cmd 530 + } 531 + 532 + func calendarCmd(h *handlers.TaskHandler) *cobra.Command { 533 + cmd := &cobra.Command{ 534 + Use: "calendar", 535 + Short: "Show tasks in calendar view", 536 + Aliases: []string{"cal"}, 537 + Long: `Display tasks with due dates in a calendar format. 538 + 539 + Shows tasks organized by week and day, making it easy to see upcoming deadlines 540 + and plan your work schedule.`, 541 + RunE: func(c *cobra.Command, args []string) error { 542 + weeks, _ := c.Flags().GetInt("weeks") 543 + defer h.Close() 544 + return h.Calendar(c.Context(), weeks) 545 + }, 546 + } 547 + cmd.Flags().IntP("weeks", "w", 4, "Number of weeks to show") 548 + return cmd 549 + } 550 + 454 551 func taskDependCmd(h *handlers.TaskHandler) *cobra.Command { 455 552 root := &cobra.Command{ 456 553 Use: "depend", ··· 514 611 root.AddCommand(addCmd, removeCmd, listCmd, blockedByCmd) 515 612 return root 516 613 } 614 + 615 + func taskAnnotateCmd(h *handlers.TaskHandler) *cobra.Command { 616 + root := &cobra.Command{ 617 + Use: "annotate", 618 + Aliases: []string{"note"}, 619 + Short: "Manage task annotations", 620 + Long: `Add, list, or remove annotations on tasks. 621 + 622 + Annotations are timestamped notes that provide context and updates 623 + about a task's progress or relevant information.`, 624 + } 625 + 626 + addCmd := &cobra.Command{ 627 + Use: "add <task-id> <annotation>", 628 + Short: "Add an annotation to a task", 629 + Aliases: []string{"create"}, 630 + Args: cobra.MinimumNArgs(2), 631 + RunE: func(c *cobra.Command, args []string) error { 632 + taskID := args[0] 633 + annotation := strings.Join(args[1:], " ") 634 + defer h.Close() 635 + return h.Annotate(c.Context(), taskID, annotation) 636 + }, 637 + } 638 + 639 + listCmd := &cobra.Command{ 640 + Use: "list <task-id>", 641 + Short: "List all annotations for a task", 642 + Aliases: []string{"ls", "show"}, 643 + Args: cobra.ExactArgs(1), 644 + RunE: func(c *cobra.Command, args []string) error { 645 + defer h.Close() 646 + return h.ListAnnotations(c.Context(), args[0]) 647 + }, 648 + } 649 + 650 + removeCmd := &cobra.Command{ 651 + Use: "remove <task-id> <index>", 652 + Short: "Remove an annotation by index", 653 + Aliases: []string{"rm", "delete"}, 654 + Args: cobra.ExactArgs(2), 655 + RunE: func(c *cobra.Command, args []string) error { 656 + taskID := args[0] 657 + index, err := strconv.Atoi(args[1]) 658 + if err != nil { 659 + return fmt.Errorf("invalid annotation index: %w", err) 660 + } 661 + defer h.Close() 662 + return h.RemoveAnnotation(c.Context(), taskID, index) 663 + }, 664 + } 665 + 666 + root.AddCommand(addCmd, listCmd, removeCmd) 667 + return root 668 + } 669 + 670 + func taskBulkEditCmd(h *handlers.TaskHandler) *cobra.Command { 671 + cmd := &cobra.Command{ 672 + Use: "bulk-edit <task-id>...", 673 + Aliases: []string{"bulk"}, 674 + Short: "Update multiple tasks at once", 675 + Long: `Update multiple tasks with the same changes. 676 + 677 + Allows batch updates to status, priority, project, context, and tags. 678 + Use --add-tags to add tags without replacing existing ones. 679 + Use --remove-tags to remove specific tags from tasks. 680 + 681 + Examples: 682 + noteleaf todo bulk-edit 1 2 3 --status done 683 + noteleaf todo bulk-edit 1 2 --project web --priority high 684 + noteleaf todo bulk-edit 1 2 3 --add-tags urgent,review`, 685 + Args: cobra.MinimumNArgs(1), 686 + RunE: func(c *cobra.Command, args []string) error { 687 + status, _ := c.Flags().GetString("status") 688 + priority, _ := c.Flags().GetString("priority") 689 + project, _ := c.Flags().GetString("project") 690 + context, _ := c.Flags().GetString("context") 691 + tags, _ := c.Flags().GetStringSlice("tags") 692 + addTags, _ := c.Flags().GetBool("add-tags") 693 + removeTags, _ := c.Flags().GetBool("remove-tags") 694 + 695 + defer h.Close() 696 + return h.BulkEdit(c.Context(), args, status, priority, project, context, tags, addTags, removeTags) 697 + }, 698 + } 699 + 700 + cmd.Flags().String("status", "", "Set status for all tasks") 701 + cmd.Flags().String("priority", "", "Set priority for all tasks") 702 + cmd.Flags().String("project", "", "Set project for all tasks") 703 + cmd.Flags().String("context", "", "Set context for all tasks") 704 + cmd.Flags().StringSlice("tags", []string{}, "Set tags for all tasks") 705 + cmd.Flags().Bool("add-tags", false, "Add tags instead of replacing") 706 + cmd.Flags().Bool("remove-tags", false, "Remove specified tags") 707 + 708 + return cmd 709 + } 710 + 711 + func taskUndoCmd(h *handlers.TaskHandler) *cobra.Command { 712 + cmd := &cobra.Command{ 713 + Use: "undo <task-id>", 714 + Short: "Undo the last change to a task", 715 + Long: `Revert a task to its previous state before the last update. 716 + 717 + This command uses the task history to restore the task to how it was 718 + before the most recent modification. 719 + 720 + Examples: 721 + noteleaf todo undo 1 722 + noteleaf todo undo abc-123-uuid`, 723 + Args: cobra.ExactArgs(1), 724 + RunE: func(c *cobra.Command, args []string) error { 725 + defer h.Close() 726 + return h.UndoTask(c.Context(), args[0]) 727 + }, 728 + } 729 + 730 + return cmd 731 + } 732 + 733 + func taskHistoryCmd(h *handlers.TaskHandler) *cobra.Command { 734 + cmd := &cobra.Command{ 735 + Use: "history <task-id>", 736 + Aliases: []string{"log"}, 737 + Short: "Show change history for a task", 738 + Long: `Display the history of changes made to a task. 739 + 740 + Shows a chronological list of modifications with timestamps. 741 + 742 + Examples: 743 + noteleaf todo history 1 744 + noteleaf todo history 1 --limit 5`, 745 + Args: cobra.ExactArgs(1), 746 + RunE: func(c *cobra.Command, args []string) error { 747 + limit, _ := c.Flags().GetInt("limit") 748 + defer h.Close() 749 + return h.ShowHistory(c.Context(), args[0], limit) 750 + }, 751 + } 752 + 753 + cmd.Flags().IntP("limit", "n", 10, "Limit number of history entries") 754 + 755 + return cmd 756 + }
+5
cmd/task_flags.go
··· 31 31 func addDueDateFlag(cmd *cobra.Command) { 32 32 cmd.Flags().StringP("due", "d", "", "Set due date (YYYY-MM-DD)") 33 33 } 34 + 35 + func addWaitScheduledFlags(cmd *cobra.Command) { 36 + cmd.Flags().StringP("wait", "w", "", "Task not actionable until date (YYYY-MM-DD)") 37 + cmd.Flags().StringP("scheduled", "s", "", "Task scheduled to start on date (YYYY-MM-DD)") 38 + }
+13
cmd/tools_dev.go
··· 1 + //go:build !prod 2 + 3 + package main 4 + 5 + import ( 6 + "github.com/spf13/cobra" 7 + "github.com/stormlightlabs/noteleaf/tools" 8 + ) 9 + 10 + // registerTools adds development tools to the root command 11 + func registerTools(root *cobra.Command) { 12 + root.AddCommand(tools.NewToolsCommand(root)) 13 + }
+8
cmd/tools_prod.go
··· 1 + //go:build prod 2 + 3 + package main 4 + 5 + import "github.com/spf13/cobra" 6 + 7 + // registerTools is a no-op in production builds 8 + func registerTools(*cobra.Command) {}
+193 -42
internal/docs/ROADMAP.md
··· 4 4 5 5 ## Core Usability 6 6 7 - The foundation across all domains is implemented. Tasks support CRUD operations, projects, tags, contexts, and time tracking. Notes have create, list, read, edit, and remove commands with interactive and static modes. Media queues exist for books, movies, and TV with progress and status management. SQLite persistence is in place with setup, seed, and reset commands. TUIs and colorized output are available. 7 + The foundation across all domains is implemented. Tasks support CRUD operations, projects, tags, contexts, and time tracking. 8 + Notes have create, list, read, edit, and remove commands with interactive and static modes. Media queues exist for books, movies, and TV with progress and status management. SQLite persistence is in place with setup, seed, and reset commands. TUIs and colorized output are available. 8 9 9 10 ## RC 10 11 ··· 43 44 #### Publication 44 45 45 46 - [x] Implement authentication with BlueSky/leaflet (AT Protocol). 46 - - [ ] Add OAuth2 47 + - [ ] Add [OAuth2](#publications--authentication) 47 48 - [x] Verify `pub pull` fetches and syncs documents from leaflet. 48 49 - [x] Confirm `pub list` with status filtering (`all`, `published`, `draft`). 49 - - [ ] Test `pub post` creates new documents with draft/preview/validate modes. 50 - - [ ] Ensure `pub patch` updates existing documents correctly. 51 - - [ ] Validate `pub push` handles batch operations (create/update). 52 - - [ ] Verify markdown conversion to leaflet block format (headings, code, images, facets). 50 + - [x] Test `pub post` creates new documents with draft/preview/validate modes. 51 + - [x] Ensure `pub patch` updates existing documents correctly. 52 + - [x] Validate `pub push` handles batch operations (create/update). 53 + - [x] Verify markdown conversion to leaflet block format (headings, code, images, facets). 53 54 54 55 ### Media Domains 55 56 ··· 109 110 - Invalid IDs 110 111 - Invalid flags 111 112 - Schema corruption (already tested in repo) 112 - - [ ] Test cross-platform behavior (Linux/macOS/Windows). 113 113 114 114 ### Packaging 115 115 ··· 125 125 126 126 ### Tasks 127 127 128 - - [ ] Model 129 - - [ ] Dependencies 130 - - [ ] Recurrence (`recur`, `until`, templates) 131 - - [ ] Wait/scheduled dates 132 - - [ ] Urgency scoring 133 - - [ ] Operations 134 - - [ ] `annotate` 135 - - [ ] Bulk edit and undo/history 136 - - [ ] `$EDITOR` integration 137 - - [ ] Reports and Views 138 - - [ ] Next actions 139 - - [ ] Completed/waiting/blocked reports 140 - - [ ] Calendar view 141 - - [ ] Sorting and urgency-based views 128 + - [x] Model 129 + - [x] Dependencies 130 + - [x] Recurrence (`recur`, `until`, templates) 131 + - [x] Wait/scheduled dates 132 + - [x] Urgency scoring 133 + - [x] Operations 134 + - [x] `annotate` 135 + - [x] Bulk edit and undo/history 136 + - [x] Reports and Views 137 + - [x] Next actions 138 + - [x] Completed/waiting/blocked reports 139 + - [x] Calendar view 140 + - [x] Sorting and urgency-based views 142 141 - [ ] Queries and Filters 143 142 - [ ] Rich query language 144 143 - [ ] Saved filters and aliases ··· 149 148 ### Notes 150 149 151 150 - [ ] Commands 152 - - [ ] `note search` 151 + - [x] `note search` - TF-IDF search via `search query` command 153 152 - [ ] `note tag` 154 153 - [ ] `note recent` 155 154 - [ ] `note templates` 156 155 - [ ] `note archive` 157 156 - [ ] `note export` 158 157 - [ ] Features 159 - - [ ] Full-text search 158 + - [x] Full-text search - TF-IDF ranking with Unicode tokenization 160 159 - [ ] Linking between notes, tasks, and media 161 160 161 + ### Search 162 + 163 + #### Ranking Improvements 164 + 165 + - [ ] BM25 scoring algorithm 166 + - [ ] Implement Okapi BM25 with configurable parameters (k1, b) 167 + - [ ] Field-aware BM25F with title/body weighting 168 + - [ ] Pluggable scoring strategy interface (TF-IDF/BM25 interchangeable) 169 + - [ ] Benchmark against TF-IDF on sample corpus 170 + 171 + #### Query Features 172 + 173 + - [ ] Phrase and proximity queries 174 + - [ ] Positional inverted index (track term positions in documents) 175 + - [ ] Exact phrase matching (`"go programming"`) 176 + - [ ] Proximity scoring (boost when terms appear near each other) 177 + - [ ] Query understanding 178 + - [ ] Synonym expansion with configurable dictionaries 179 + - [ ] Boolean operators (AND, OR, NOT) 180 + - [ ] Field-specific queries (`title:golang body:concurrency`) 181 + - [ ] Spelling correction with edit distance suggestions 182 + - [ ] Query boosting syntax (`title^3 golang`) 183 + 184 + #### Linguistic Processing 185 + 186 + - [ ] Text normalization 187 + - [ ] Porter stemmer for English (run/runs/running โ†’ run) 188 + - [ ] Stopword filtering with domain-specific lists 189 + - [ ] Unicode normalization and diacritic folding 190 + - [ ] Configurable token filter pipeline 191 + - [ ] Multi-language support 192 + - [ ] Language detection 193 + - [ ] Language-specific stemmers 194 + - [ ] CJK tokenization improvements 195 + 196 + #### Advanced Scoring 197 + 198 + - [ ] Learning to Rank 199 + - [ ] Feature extraction (TF-IDF/BM25 scores, term coverage, recency) 200 + - [ ] Click-through rate tracking for relevance feedback 201 + - [ ] Gradient-boosted tree models for re-ranking 202 + - [ ] Evaluation metrics (NDCG, MAP) 203 + - [ ] Non-text signals 204 + - [ ] Document recency scoring 205 + - [ ] Tag-based relevance 206 + - [ ] User interaction signals 207 + 208 + #### Index Management 209 + 210 + - [ ] Persistence and optimization 211 + - [ ] On-disk index snapshots (gob serialization) 212 + - [ ] Segmented indexing with periodic merging (Lucene-style) 213 + - [ ] Incremental updates (add/update/delete without full rebuild) 214 + - [ ] Index versioning and rollback 215 + - [ ] Compression for large corpora 216 + - [ ] Performance 217 + - [ ] Index build benchmarks vs corpus size 218 + - [ ] Query latency monitoring 219 + - [ ] Memory usage profiling 220 + - [ ] Concurrent search support 221 + 222 + #### User Experience 223 + 224 + - [ ] Interactive search interface 225 + - [ ] TUI with real-time search-as-you-type 226 + - [ ] Result navigation with vim keybindings 227 + - [ ] Preview pane showing full note content 228 + - [ ] Filtering by tags, date ranges, doc kind 229 + - [ ] Sort options (relevance, date, alphabetical) 230 + - [ ] Quick actions (open in editor, copy ID, tag) 231 + - [ ] Search result display 232 + - [ ] Snippet generation with matched term highlighting 233 + - [ ] Configurable result limit and pagination 234 + - [ ] Score explanation mode (`--explain` flag) 235 + - [ ] Export results to JSON/CSV 236 + - [ ] CLI 237 + - [ ] Saved search queries and aliases 238 + - [ ] Search history 239 + - [ ] Query latency and result count in output 240 + - [ ] Color-coded relevance scores 241 + 162 242 ### Media 163 243 164 244 - [ ] Articles/papers/blogs ··· 199 279 - [ ] External imports (Goodreads, IMDB, Letterboxd) 200 280 - [ ] Cross-referencing across media types 201 281 - [ ] Analytics: velocity, completion rates 282 + - [ ] Books (Open Library integration enhancements) 283 + - [ ] Author detail fetching (full names, bio) 284 + - [ ] Edition-specific metadata 285 + - [ ] Cover image download and caching 286 + - [ ] Reading progress tracking 287 + - [ ] Personal reading lists sync 288 + - [ ] Movies/TV (external API integration) 289 + - [ ] Movie databases (TMDb, OMDb) 290 + - [ ] Rotten Tomatoes integration 291 + - [ ] Music 292 + - [ ] Music services (MusicBrainz, Album of the Year) 202 293 203 294 ### Articles 204 295 ··· 206 297 - [ ] Export to multiple formats 207 298 - [ ] Linking with tasks and notes 208 299 300 + ### Publications & Authentication 301 + 302 + - [ ] OAuth2 authentication for AT Protocol 303 + - [ ] Client metadata server for publishing application details 304 + - [ ] DPoP (Demonstrating Proof of Possession) implementation 305 + - [ ] ES256 JWT generation with unique JTI nonces 306 + - [ ] Server-issued nonce management with 5-minute rotation 307 + - [ ] Separate nonce tracking for authorization and resource servers 308 + - [ ] PAR (Pushed Authorization Requests) flow 309 + - [ ] PKCE code challenge generation 310 + - [ ] State token management 311 + - [ ] Request URI handling 312 + - [ ] Identity resolution and verification 313 + - [ ] Bidirectional handle verification 314 + - [ ] DID resolution from handles 315 + - [ ] Authorization server discovery via .well-known endpoints 316 + - [ ] Token lifecycle management 317 + - [ ] Access token refresh (5-15 min lifetime recommended) 318 + - [ ] Refresh token rotation (180 day max for confidential clients) 319 + - [ ] Concurrent request handling to prevent duplicate refreshes 320 + - [ ] Secure token storage (encrypted at rest) 321 + - [ ] Local callback server for OAuth redirects 322 + - [ ] Ephemeral HTTP server on localhost 323 + - [ ] Browser launch integration 324 + - [ ] Timeout handling for abandoned flows 325 + - [ ] Support both OAuth & App Passwords but recommend OAuth 326 + - [ ] Leaflet.pub enhancements 327 + - [ ] Multiple Publications: Manage separate publications for different topics 328 + - [ ] Image Upload: Automatically upload images to blob storage and embed in documents 329 + - [ ] Status Management: Publish drafts and unpublish documents from CLI 330 + - [ ] Metadata Editing: Update document titles, summaries, and tags 331 + - [ ] Backlink Support: Parse and resolve cross-references between documents 332 + - [ ] Offline Mode: Queue posts and patches for later upload 333 + 209 334 ### User Experience 210 335 211 336 - [ ] Shell completions 212 337 - [ ] Manpages and docs generator 213 338 - [ ] Theming and customizable output 214 339 - [ ] Calendar integration 340 + - [ ] Task synchronization services 341 + - [ ] Git repository linking 342 + - [ ] Note export to other platforms 215 343 216 344 ### Tasks 217 345 ··· 243 371 244 372 ### Local API Server 245 373 246 - A local HTTP server daemon that exposes Noteleaf data for web UIs and extensions. Runs on the user's machine and provides programmatic access to tasks, notes, and media. 374 + A local HTTP server daemon that exposes Noteleaf data for web UIs and extensions. 375 + Runs on the user's machine and provides programmatic access to tasks, notes, and media. 247 376 248 377 #### Architecture 249 378 ··· 371 500 #### Post v1 372 501 373 502 - Backup/restore 503 + - [ ] Automated backups 504 + - [ ] Backup scheduling and rotation 374 505 - Multiple profiles 375 506 - Optional synchronization 507 + - [ ] Sync service 508 + - Import/Export 509 + - [ ] CSV export for tasks 510 + - [ ] Markdown export for tasks 511 + - [ ] Bulk export commands 512 + - [ ] Migration utilities (TaskWarrior, todo.txt, etc.) 513 + - [ ] Git integration for notes/data versioning 376 514 377 515 ## v1 Feature Matrix 378 516 379 - | Domain | Feature | Status | 380 - |----------|-----------------------|-----------| 381 - | Tasks | CRUD | Complete | 382 - | Tasks | Projects/tags | Complete | 383 - | Tasks | Time tracking | Complete | 384 - | Tasks | Dependencies | Complete | 385 - | Tasks | Recurrence | Complete | 386 - | Tasks | Wait/scheduled | Planned | 387 - | Tasks | Urgency scoring | Planned | 388 - | Notes | CRUD | Complete | 389 - | Notes | Search/tagging | Planned | 390 - | Media | Books/movies/TV | Complete | 391 - | Media | Articles | Complete | 392 - | Media | Source/ratings | Planned | 393 - | Articles | Parser + storage | Complete | 394 - | System | SQLite persistence | Complete | 395 - | System | Synchronization | Future | 396 - | System | Import/export formats | Future | 517 + | Domain | Feature | Status | 518 + |--------------|----------------------------|-----------| 519 + | Tasks | CRUD | Complete | 520 + | Tasks | Projects/tags | Complete | 521 + | Tasks | Time tracking | Complete | 522 + | Tasks | Dependencies | Complete | 523 + | Tasks | Recurrence | Complete | 524 + | Tasks | Wait/scheduled | Complete | 525 + | Tasks | Urgency scoring | Complete | 526 + | Tasks | Reports and views | Complete | 527 + | Notes | CRUD | Complete | 528 + | Notes | Search (TF-IDF) | Complete | 529 + | Notes | Advanced search | Planned | 530 + | Notes | Tagging | Planned | 531 + | Publications | AT Protocol sync | Complete | 532 + | Publications | Post/patch/push | Complete | 533 + | Publications | Markdown conversion | Complete | 534 + | Publications | OAuth2 | Future | 535 + | Media | Books/movies/TV | Complete | 536 + | Media | Articles | Complete | 537 + | Media | Source/ratings | Planned | 538 + | Articles | Parser + storage | Complete | 539 + | System | SQLite persistence | Complete | 540 + | System | Configuration management | Complete | 541 + | System | Synchronization | Future | 542 + | System | Import/export formats | Future | 543 + 544 + ## Parking Lot 545 + 546 + - [ ] Test cross-platform behavior (Linux/macOS/Windows). 547 + - [ ] `$EDITOR` integration
+153
internal/documents/documents.go
··· 1 + // Term Frequency-Inverse Document Frequency search model for notes 2 + package documents 3 + 4 + import ( 5 + "math" 6 + "regexp" 7 + "sort" 8 + "strings" 9 + "time" 10 + ) 11 + 12 + type DocKind int64 13 + 14 + const ( 15 + NoteDoc DocKind = iota 16 + ArticleDoc 17 + MovieDoc 18 + BookDoc 19 + TVDoc 20 + ) 21 + 22 + type Document struct { 23 + ID int64 24 + Title string 25 + Body string 26 + CreatedAt time.Time 27 + DocKind int64 28 + } 29 + 30 + type Posting struct { 31 + DocID int64 32 + TF int 33 + } 34 + 35 + type Index struct { 36 + Postings map[string][]Posting 37 + DocLengths map[int64]int 38 + NumDocs int 39 + } 40 + 41 + type Result struct { 42 + DocID int64 43 + Score float64 44 + } 45 + 46 + type Searchable interface { 47 + Search(query string, limit int) ([]Result, error) 48 + } 49 + 50 + // Tokenizer handles text tokenization and normalization 51 + type Tokenizer struct { 52 + pattern *regexp.Regexp 53 + } 54 + 55 + // NewTokenizer creates a new tokenizer with Unicode-aware word/number matching 56 + func NewTokenizer() *Tokenizer { 57 + return &Tokenizer{ 58 + pattern: regexp.MustCompile(`\p{L}+\p{M}*|\p{N}+`), 59 + } 60 + } 61 + 62 + // Tokenize splits text into normalized tokens (lowercase words and numbers) 63 + func (t *Tokenizer) Tokenize(text string) []string { 64 + lowered := strings.ToLower(text) 65 + return t.pattern.FindAllString(lowered, -1) 66 + } 67 + 68 + // TokenFrequency computes term frequency map for tokens 69 + func TokenFrequency(tokens []string) map[string]int { 70 + freq := make(map[string]int) 71 + for _, token := range tokens { 72 + freq[token]++ 73 + } 74 + return freq 75 + } 76 + 77 + // BuildIndex constructs a TF-IDF index from a collection of documents 78 + func BuildIndex(docs []Document) *Index { 79 + idx := &Index{ 80 + Postings: make(map[string][]Posting), 81 + DocLengths: make(map[int64]int), 82 + NumDocs: 0, 83 + } 84 + 85 + tokenizer := NewTokenizer() 86 + 87 + for _, doc := range docs { 88 + text := doc.Title + " " + doc.Body 89 + tokens := tokenizer.Tokenize(text) 90 + 91 + idx.NumDocs++ 92 + idx.DocLengths[doc.ID] = len(tokens) 93 + 94 + freq := TokenFrequency(tokens) 95 + 96 + for term, tf := range freq { 97 + idx.Postings[term] = append(idx.Postings[term], Posting{ 98 + DocID: doc.ID, 99 + TF: tf, 100 + }) 101 + } 102 + } 103 + 104 + return idx 105 + } 106 + 107 + // Search performs TF-IDF ranked search on the index 108 + func (idx *Index) Search(query string, limit int) ([]Result, error) { 109 + tokenizer := NewTokenizer() 110 + queryTokens := tokenizer.Tokenize(query) 111 + 112 + if len(queryTokens) == 0 { 113 + return []Result{}, nil 114 + } 115 + 116 + scores := make(map[int64]float64) 117 + 118 + for _, term := range queryTokens { 119 + postings, exists := idx.Postings[term] 120 + if !exists { 121 + continue 122 + } 123 + 124 + df := len(postings) 125 + idf := math.Log(float64(idx.NumDocs) / float64(df)) 126 + 127 + for _, posting := range postings { 128 + tf := float64(posting.TF) 129 + scores[posting.DocID] += tf * idf 130 + } 131 + } 132 + 133 + results := make([]Result, 0, len(scores)) 134 + for docID, score := range scores { 135 + results = append(results, Result{ 136 + DocID: docID, 137 + Score: score, 138 + }) 139 + } 140 + 141 + sort.Slice(results, func(i, j int) bool { 142 + if results[i].Score != results[j].Score { 143 + return results[i].Score > results[j].Score 144 + } 145 + return results[i].DocID > results[j].DocID 146 + }) 147 + 148 + if limit > 0 && limit < len(results) { 149 + results = results[:limit] 150 + } 151 + 152 + return results, nil 153 + }
+380
internal/documents/documents_test.go
··· 1 + package documents 2 + 3 + import ( 4 + "testing" 5 + "time" 6 + ) 7 + 8 + func TestTokenizer(t *testing.T) { 9 + tokenizer := NewTokenizer() 10 + 11 + t.Run("Basic tokenization", func(t *testing.T) { 12 + t.Run("tokenizes simple text", func(t *testing.T) { 13 + tokens := tokenizer.Tokenize("Hello World") 14 + if len(tokens) != 2 { 15 + t.Fatalf("expected 2 tokens, got %d", len(tokens)) 16 + } 17 + if tokens[0] != "hello" || tokens[1] != "world" { 18 + t.Errorf("expected [hello world], got %v", tokens) 19 + } 20 + }) 21 + 22 + t.Run("lowercases all tokens", func(t *testing.T) { 23 + tokens := tokenizer.Tokenize("UPPERCASE MiXeD lowercase") 24 + if len(tokens) != 3 { 25 + t.Fatalf("expected 3 tokens, got %d", len(tokens)) 26 + } 27 + for _, token := range tokens { 28 + if token != "uppercase" && token != "mixed" && token != "lowercase" { 29 + t.Errorf("unexpected token: %s", token) 30 + } 31 + } 32 + }) 33 + 34 + t.Run("handles punctuation", func(t *testing.T) { 35 + tokens := tokenizer.Tokenize("Hello, world! How are you?") 36 + expected := []string{"hello", "world", "how", "are", "you"} 37 + if len(tokens) != len(expected) { 38 + t.Fatalf("expected %d tokens, got %d", len(expected), len(tokens)) 39 + } 40 + for i, token := range tokens { 41 + if token != expected[i] { 42 + t.Errorf("token %d: expected %s, got %s", i, expected[i], token) 43 + } 44 + } 45 + }) 46 + }) 47 + 48 + t.Run("Unicode support", func(t *testing.T) { 49 + t.Run("tokenizes unicode characters", func(t *testing.T) { 50 + tokens := tokenizer.Tokenize("cafรฉ rรฉsumรฉ naรฏve") 51 + if len(tokens) != 3 { 52 + t.Fatalf("expected 3 tokens, got %d", len(tokens)) 53 + } 54 + }) 55 + 56 + t.Run("handles emoji and special characters", func(t *testing.T) { 57 + tokens := tokenizer.Tokenize("hello ๐Ÿ˜€ world") 58 + if len(tokens) != 2 { 59 + t.Fatalf("expected 2 tokens (emoji excluded), got %d", len(tokens)) 60 + } 61 + if tokens[0] != "hello" || tokens[1] != "world" { 62 + t.Errorf("expected [hello world], got %v", tokens) 63 + } 64 + }) 65 + 66 + t.Run("tokenizes CJK characters", func(t *testing.T) { 67 + tokens := tokenizer.Tokenize("ไฝ ๅฅฝ ไธ–็•Œ") 68 + if len(tokens) != 2 { 69 + t.Fatalf("expected 2 tokens, got %d", len(tokens)) 70 + } 71 + }) 72 + }) 73 + 74 + t.Run("Numbers", func(t *testing.T) { 75 + t.Run("tokenizes numbers", func(t *testing.T) { 76 + tokens := tokenizer.Tokenize("test 123 456") 77 + if len(tokens) != 3 { 78 + t.Fatalf("expected 3 tokens, got %d", len(tokens)) 79 + } 80 + if tokens[1] != "123" || tokens[2] != "456" { 81 + t.Errorf("expected numbers to be tokenized, got %v", tokens) 82 + } 83 + }) 84 + 85 + t.Run("handles mixed alphanumeric", func(t *testing.T) { 86 + tokens := tokenizer.Tokenize("version 2 released") 87 + if len(tokens) != 3 { 88 + t.Fatalf("expected 3 tokens, got %d", len(tokens)) 89 + } 90 + }) 91 + }) 92 + 93 + t.Run("Edge cases", func(t *testing.T) { 94 + t.Run("handles empty string", func(t *testing.T) { 95 + tokens := tokenizer.Tokenize("") 96 + if len(tokens) != 0 { 97 + t.Errorf("expected 0 tokens for empty string, got %d", len(tokens)) 98 + } 99 + }) 100 + 101 + t.Run("handles whitespace only", func(t *testing.T) { 102 + tokens := tokenizer.Tokenize(" \t\n ") 103 + if len(tokens) != 0 { 104 + t.Errorf("expected 0 tokens for whitespace, got %d", len(tokens)) 105 + } 106 + }) 107 + 108 + t.Run("handles punctuation only", func(t *testing.T) { 109 + tokens := tokenizer.Tokenize("!@#$%^&*()") 110 + if len(tokens) != 0 { 111 + t.Errorf("expected 0 tokens for punctuation only, got %d", len(tokens)) 112 + } 113 + }) 114 + }) 115 + } 116 + 117 + func TestTokenFrequency(t *testing.T) { 118 + t.Run("counts term frequencies", func(t *testing.T) { 119 + tokens := []string{"hello", "world", "hello", "test"} 120 + freq := TokenFrequency(tokens) 121 + 122 + if freq["hello"] != 2 { 123 + t.Errorf("expected hello frequency 2, got %d", freq["hello"]) 124 + } 125 + if freq["world"] != 1 { 126 + t.Errorf("expected world frequency 1, got %d", freq["world"]) 127 + } 128 + if freq["test"] != 1 { 129 + t.Errorf("expected test frequency 1, got %d", freq["test"]) 130 + } 131 + }) 132 + 133 + t.Run("handles empty token list", func(t *testing.T) { 134 + freq := TokenFrequency([]string{}) 135 + if len(freq) != 0 { 136 + t.Errorf("expected empty frequency map, got %d entries", len(freq)) 137 + } 138 + }) 139 + 140 + t.Run("handles single token", func(t *testing.T) { 141 + freq := TokenFrequency([]string{"single"}) 142 + if freq["single"] != 1 { 143 + t.Errorf("expected frequency 1, got %d", freq["single"]) 144 + } 145 + }) 146 + } 147 + 148 + func TestBuildIndex(t *testing.T) { 149 + now := time.Now() 150 + 151 + t.Run("builds index from documents", func(t *testing.T) { 152 + docs := []Document{ 153 + {ID: 1, Title: "Go Programming", Body: "Go is a great language", CreatedAt: now, DocKind: int64(NoteDoc)}, 154 + {ID: 2, Title: "Python Guide", Body: "Python is versatile", CreatedAt: now, DocKind: int64(ArticleDoc)}, 155 + } 156 + 157 + idx := BuildIndex(docs) 158 + 159 + if idx.NumDocs != 2 { 160 + t.Errorf("expected NumDocs 2, got %d", idx.NumDocs) 161 + } 162 + 163 + if len(idx.DocLengths) != 2 { 164 + t.Errorf("expected 2 document lengths, got %d", len(idx.DocLengths)) 165 + } 166 + 167 + if idx.DocLengths[1] <= 0 || idx.DocLengths[2] <= 0 { 168 + t.Error("document lengths should be positive") 169 + } 170 + 171 + if _, exists := idx.Postings["go"]; !exists { 172 + t.Error("expected 'go' to be in postings") 173 + } 174 + if _, exists := idx.Postings["python"]; !exists { 175 + t.Error("expected 'python' to be in postings") 176 + } 177 + }) 178 + 179 + t.Run("handles empty document list", func(t *testing.T) { 180 + idx := BuildIndex([]Document{}) 181 + if idx.NumDocs != 0 { 182 + t.Errorf("expected NumDocs 0, got %d", idx.NumDocs) 183 + } 184 + if len(idx.Postings) != 0 { 185 + t.Errorf("expected empty postings, got %d entries", len(idx.Postings)) 186 + } 187 + }) 188 + 189 + t.Run("calculates term frequencies correctly", func(t *testing.T) { 190 + docs := []Document{ 191 + {ID: 1, Title: "test", Body: "test test test", CreatedAt: now, DocKind: int64(NoteDoc)}, 192 + } 193 + 194 + idx := BuildIndex(docs) 195 + 196 + postings := idx.Postings["test"] 197 + if len(postings) != 1 { 198 + t.Fatalf("expected 1 posting for 'test', got %d", len(postings)) 199 + } 200 + 201 + if postings[0].TF != 4 { 202 + t.Errorf("expected TF 4 (title + 3 in body), got %d", postings[0].TF) 203 + } 204 + }) 205 + 206 + t.Run("builds postings for multiple documents with same term", func(t *testing.T) { 207 + docs := []Document{ 208 + {ID: 1, Title: "Go", Body: "Go is great", CreatedAt: now, DocKind: int64(NoteDoc)}, 209 + {ID: 2, Title: "Go Tutorial", Body: "Learn Go", CreatedAt: now, DocKind: int64(NoteDoc)}, 210 + } 211 + 212 + idx := BuildIndex(docs) 213 + 214 + postings := idx.Postings["go"] 215 + if len(postings) != 2 { 216 + t.Fatalf("expected 2 postings for 'go', got %d", len(postings)) 217 + } 218 + }) 219 + } 220 + 221 + func TestIndexSearch(t *testing.T) { 222 + now := time.Now() 223 + 224 + t.Run("Search functionality", func(t *testing.T) { 225 + t.Run("returns empty results for empty query", func(t *testing.T) { 226 + docs := []Document{ 227 + {ID: 1, Title: "Test", Body: "content", CreatedAt: now, DocKind: int64(NoteDoc)}, 228 + } 229 + idx := BuildIndex(docs) 230 + 231 + results, err := idx.Search("", 10) 232 + if err != nil { 233 + t.Fatalf("unexpected error: %v", err) 234 + } 235 + if len(results) != 0 { 236 + t.Errorf("expected 0 results for empty query, got %d", len(results)) 237 + } 238 + }) 239 + 240 + t.Run("finds matching documents", func(t *testing.T) { 241 + docs := []Document{ 242 + {ID: 1, Title: "Go Programming", Body: "Learn Go language", CreatedAt: now, DocKind: int64(NoteDoc)}, 243 + {ID: 2, Title: "Python Guide", Body: "Python is versatile", CreatedAt: now, DocKind: int64(ArticleDoc)}, 244 + } 245 + idx := BuildIndex(docs) 246 + 247 + results, err := idx.Search("go", 10) 248 + if err != nil { 249 + t.Fatalf("unexpected error: %v", err) 250 + } 251 + 252 + if len(results) != 1 { 253 + t.Fatalf("expected 1 result, got %d", len(results)) 254 + } 255 + 256 + if results[0].DocID != 1 { 257 + t.Errorf("expected DocID 1, got %d", results[0].DocID) 258 + } 259 + 260 + if results[0].Score <= 0 { 261 + t.Error("expected positive score") 262 + } 263 + }) 264 + 265 + t.Run("ranks documents by relevance", func(t *testing.T) { 266 + docs := []Document{ 267 + {ID: 1, Title: "Go", Body: "tutorial python rust", CreatedAt: now, DocKind: int64(NoteDoc)}, 268 + {ID: 2, Title: "Go Programming", Body: "advanced go tutorial", CreatedAt: now, DocKind: int64(NoteDoc)}, 269 + {ID: 3, Title: "Python", Body: "different language", CreatedAt: now, DocKind: int64(NoteDoc)}, 270 + } 271 + idx := BuildIndex(docs) 272 + 273 + results, err := idx.Search("go", 10) 274 + if err != nil { 275 + t.Fatalf("unexpected error: %v", err) 276 + } 277 + 278 + if len(results) != 2 { 279 + t.Fatalf("expected 2 results, got %d", len(results)) 280 + } 281 + 282 + if results[0].DocID != 2 { 283 + t.Errorf("expected document 2 to rank higher (has more 'go' terms)") 284 + } 285 + 286 + if results[0].Score <= results[1].Score { 287 + t.Errorf("expected first result to have higher score, got %f <= %f", results[0].Score, results[1].Score) 288 + } 289 + }) 290 + 291 + t.Run("respects limit parameter", func(t *testing.T) { 292 + docs := []Document{ 293 + {ID: 1, Title: "test one", Body: "content", CreatedAt: now, DocKind: int64(NoteDoc)}, 294 + {ID: 2, Title: "test two", Body: "content", CreatedAt: now, DocKind: int64(NoteDoc)}, 295 + {ID: 3, Title: "test three", Body: "content", CreatedAt: now, DocKind: int64(NoteDoc)}, 296 + } 297 + idx := BuildIndex(docs) 298 + 299 + results, err := idx.Search("test", 2) 300 + if err != nil { 301 + t.Fatalf("unexpected error: %v", err) 302 + } 303 + 304 + if len(results) != 2 { 305 + t.Errorf("expected 2 results with limit=2, got %d", len(results)) 306 + } 307 + }) 308 + 309 + t.Run("handles multi-term queries", func(t *testing.T) { 310 + docs := []Document{ 311 + {ID: 1, Title: "Go Programming", Body: "advanced tutorial", CreatedAt: now, DocKind: int64(NoteDoc)}, 312 + {ID: 2, Title: "Go Basics", Body: "beginner tutorial", CreatedAt: now, DocKind: int64(NoteDoc)}, 313 + {ID: 3, Title: "Python", Body: "different language", CreatedAt: now, DocKind: int64(NoteDoc)}, 314 + } 315 + idx := BuildIndex(docs) 316 + 317 + results, err := idx.Search("go tutorial", 10) 318 + if err != nil { 319 + t.Fatalf("unexpected error: %v", err) 320 + } 321 + 322 + if len(results) != 2 { 323 + t.Errorf("expected 2 results, got %d", len(results)) 324 + } 325 + }) 326 + 327 + t.Run("returns no results for non-matching query", func(t *testing.T) { 328 + docs := []Document{ 329 + {ID: 1, Title: "Go", Body: "programming", CreatedAt: now, DocKind: int64(NoteDoc)}, 330 + } 331 + idx := BuildIndex(docs) 332 + 333 + results, err := idx.Search("rust", 10) 334 + if err != nil { 335 + t.Fatalf("unexpected error: %v", err) 336 + } 337 + 338 + if len(results) != 0 { 339 + t.Errorf("expected 0 results for non-matching query, got %d", len(results)) 340 + } 341 + }) 342 + 343 + t.Run("handles zero limit", func(t *testing.T) { 344 + docs := []Document{ 345 + {ID: 1, Title: "test", Body: "content", CreatedAt: now, DocKind: int64(NoteDoc)}, 346 + } 347 + idx := BuildIndex(docs) 348 + 349 + results, err := idx.Search("test", 0) 350 + if err != nil { 351 + t.Fatalf("unexpected error: %v", err) 352 + } 353 + 354 + if len(results) != 1 { 355 + t.Errorf("expected all results with limit=0, got %d", len(results)) 356 + } 357 + }) 358 + 359 + t.Run("tie-breaking uses DocID", func(t *testing.T) { 360 + docs := []Document{ 361 + {ID: 1, Title: "test", Body: "content", CreatedAt: now, DocKind: int64(NoteDoc)}, 362 + {ID: 2, Title: "test", Body: "content", CreatedAt: now, DocKind: int64(NoteDoc)}, 363 + } 364 + idx := BuildIndex(docs) 365 + 366 + results, err := idx.Search("test", 10) 367 + if err != nil { 368 + t.Fatalf("unexpected error: %v", err) 369 + } 370 + 371 + if len(results) != 2 { 372 + t.Fatalf("expected 2 results, got %d", len(results)) 373 + } 374 + 375 + if results[0].DocID <= results[1].DocID { 376 + t.Error("expected higher DocID first when scores are equal") 377 + } 378 + }) 379 + }) 380 + }
+1
internal/documents/test_utilities.go
··· 1 + package documents
+8 -8
internal/handlers/articles.go
··· 112 112 func (h *ArticleHandler) Add(ctx context.Context, url string) error { 113 113 existing, err := h.repos.Articles.GetByURL(ctx, url) 114 114 if err == nil { 115 - ui.Warningln("Article already exists: %s (ID: %d)", ui.TitleColorStyle.Render(existing.Title), existing.ID) 115 + ui.Warningln("Article already exists: %s (ID: %d)", ui.TableTitleStyle.Render(existing.Title), existing.ID) 116 116 return nil 117 117 } 118 118 ··· 153 153 154 154 ui.Infoln("Article saved successfully!") 155 155 ui.Infoln("ID: %d", id) 156 - ui.Infoln("Title: %s", ui.TitleColorStyle.Render(article.Title)) 156 + ui.Infoln("Title: %s", ui.TableTitleStyle.Render(article.Title)) 157 157 if article.Author != "" { 158 - ui.Infoln("Author: %s", ui.HeaderColorStyle.Render(article.Author)) 158 + ui.Infoln("Author: %s", ui.TableHeaderStyle.Render(article.Author)) 159 159 } 160 160 if article.Date != "" { 161 161 ui.Infoln("Date: %s", article.Date) ··· 187 187 ui.Infoln("Found %d article(s):\n", len(articles)) 188 188 for _, article := range articles { 189 189 ui.Infoln("ID: %d", article.ID) 190 - ui.Infoln("Title: %s", ui.TitleColorStyle.Render(article.Title)) 190 + ui.Infoln("Title: %s", ui.TableTitleStyle.Render(article.Title)) 191 191 if article.Author != "" { 192 - ui.Infoln("Author: %s", ui.HeaderColorStyle.Render(article.Author)) 192 + ui.Infoln("Author: %s", ui.TableHeaderStyle.Render(article.Author)) 193 193 } 194 194 if article.Date != "" { 195 195 ui.Infoln("Date: %s", article.Date) ··· 208 208 return fmt.Errorf("failed to get article: %w", err) 209 209 } 210 210 211 - ui.Infoln("Title: %s", ui.TitleColorStyle.Render(article.Title)) 211 + ui.Infoln("Title: %s", ui.TableTitleStyle.Render(article.Title)) 212 212 if article.Author != "" { 213 - ui.Infoln("Author: %s", ui.HeaderColorStyle.Render(article.Author)) 213 + ui.Infoln("Author: %s", ui.TableHeaderStyle.Render(article.Author)) 214 214 } 215 215 if article.Date != "" { 216 216 ui.Infoln("Date: %s", article.Date) ··· 302 302 if err != nil { 303 303 return fmt.Errorf("failed to get storage directory: %w", err) 304 304 } 305 - ui.Headerln("%s %s", ui.HeaderColorStyle.Render("Storage directory:"), dir) 305 + ui.Headerln("%s %s", ui.TableHeaderStyle.Render("Storage directory:"), dir) 306 306 307 307 return nil 308 308 }
+85
internal/handlers/documents.go
··· 1 + package handlers 2 + 3 + import ( 4 + "context" 5 + "database/sql" 6 + "fmt" 7 + 8 + "github.com/stormlightlabs/noteleaf/internal/repo" 9 + "github.com/stormlightlabs/noteleaf/internal/store" 10 + ) 11 + 12 + // DocumentHandler provides operations for document search 13 + type DocumentHandler struct { 14 + db *sql.DB 15 + repos *repo.Repositories 16 + engine *repo.SearchEngine 17 + } 18 + 19 + // NewDocumentHandler creates a new document handler 20 + func NewDocumentHandler() (*DocumentHandler, error) { 21 + db, err := store.NewDatabase() 22 + if err != nil { 23 + return nil, fmt.Errorf("failed to initialize database: %w", err) 24 + } 25 + 26 + repos := repo.NewRepositories(db.DB) 27 + engine := repo.NewSearchEngine(repos.Documents) 28 + 29 + return &DocumentHandler{ 30 + db: db.DB, 31 + repos: repos, 32 + engine: engine, 33 + }, nil 34 + } 35 + 36 + // RebuildIndex rebuilds the search index from notes 37 + func (h *DocumentHandler) RebuildIndex(ctx context.Context) error { 38 + if err := h.repos.Documents.DeleteAll(ctx); err != nil { 39 + return fmt.Errorf("failed to clear documents: %w", err) 40 + } 41 + 42 + if err := h.repos.Documents.RebuildFromNotes(ctx, h.repos.Notes); err != nil { 43 + return fmt.Errorf("failed to rebuild from notes: %w", err) 44 + } 45 + 46 + if err := h.engine.Rebuild(ctx); err != nil { 47 + return fmt.Errorf("failed to rebuild search index: %w", err) 48 + } 49 + 50 + fmt.Println("Search index rebuilt successfully") 51 + return nil 52 + } 53 + 54 + // Search performs a TF-IDF search and displays results 55 + func (h *DocumentHandler) Search(ctx context.Context, query string, limit int) error { 56 + if err := h.engine.Rebuild(ctx); err != nil { 57 + return fmt.Errorf("failed to rebuild index: %w", err) 58 + } 59 + 60 + results, docs, err := h.engine.SearchWithScores(ctx, query, limit) 61 + if err != nil { 62 + return fmt.Errorf("search failed: %w", err) 63 + } 64 + 65 + if len(results) == 0 { 66 + fmt.Println("No results found") 67 + return nil 68 + } 69 + 70 + fmt.Printf("Found %d results:\n\n", len(results)) 71 + for i, doc := range docs { 72 + score := results[i].Score 73 + fmt.Printf("%d. [Score: %.2f] %s\n", i+1, score, doc.Title) 74 + fmt.Printf(" %s\n\n", truncate(doc.Body, 100)) 75 + } 76 + 77 + return nil 78 + } 79 + 80 + func truncate(s string, maxLen int) string { 81 + if len(s) <= maxLen { 82 + return s 83 + } 84 + return s[:maxLen] + "..." 85 + }
+287 -20
internal/handlers/publication.go
··· 3 3 4 4 import ( 5 5 "context" 6 + "encoding/json" 6 7 "fmt" 8 + "os" 7 9 "path/filepath" 10 + "strings" 8 11 "time" 9 12 13 + "github.com/charmbracelet/log" 10 14 "github.com/stormlightlabs/noteleaf/internal/models" 11 15 "github.com/stormlightlabs/noteleaf/internal/public" 12 16 "github.com/stormlightlabs/noteleaf/internal/repo" 13 17 "github.com/stormlightlabs/noteleaf/internal/services" 18 + "github.com/stormlightlabs/noteleaf/internal/shared" 14 19 "github.com/stormlightlabs/noteleaf/internal/store" 15 20 "github.com/stormlightlabs/noteleaf/internal/ui" 16 21 ) ··· 21 26 config *store.Config 22 27 repos *repo.Repositories 23 28 atproto services.ATProtoClient 29 + debug *log.Logger 24 30 } 25 31 26 32 // NewPublicationHandler creates a new publication handler ··· 38 44 repos := repo.NewRepositories(db.DB) 39 45 atproto := services.NewATProtoService() 40 46 47 + d, _ := store.GetConfigDir() 48 + debug := shared.NewDebugLoggerWithFile(d) 49 + 41 50 if config.ATProtoDID != "" && config.ATProtoAccessJWT != "" && config.ATProtoRefreshJWT != "" { 42 51 session, err := sessionFromConfig(config) 43 52 if err == nil { 44 - _ = atproto.RestoreSession(session) 53 + if err := atproto.RestoreSession(session); err == nil { 54 + updatedSession, _ := atproto.GetSession() 55 + if updatedSession != nil { 56 + config.ATProtoAccessJWT = updatedSession.AccessJWT 57 + config.ATProtoRefreshJWT = updatedSession.RefreshJWT 58 + config.ATProtoExpiresAt = updatedSession.ExpiresAt.Format("2006-01-02T15:04:05Z07:00") 59 + _ = store.SaveConfig(config) 60 + } 61 + } 45 62 } 46 63 } 47 64 ··· 50 67 config: config, 51 68 repos: repos, 52 69 atproto: atproto, 70 + debug: debug, 53 71 }, nil 54 72 } 55 73 ··· 269 287 if err != nil { 270 288 return err 271 289 } 272 - 273 290 ui.Infoln("Creating document '%s' on leaflet...", note.Title) 274 291 275 292 result, err := h.atproto.PostDocument(ctx, *doc, isDraft) ··· 382 399 return nil 383 400 } 384 401 402 + // createNoteFromFile creates a note from a markdown file and returns its ID 403 + func (h *PublicationHandler) createNoteFromFile(ctx context.Context, filePath string) (int64, error) { 404 + if _, err := os.Stat(filePath); os.IsNotExist(err) { 405 + return 0, fmt.Errorf("file does not exist: %s", filePath) 406 + } 407 + 408 + content, err := os.ReadFile(filePath) 409 + if err != nil { 410 + return 0, fmt.Errorf("failed to read file: %w", err) 411 + } 412 + 413 + contentStr := string(content) 414 + if strings.TrimSpace(contentStr) == "" { 415 + return 0, fmt.Errorf("file is empty: %s", filePath) 416 + } 417 + 418 + title, noteContent, tags := parseNoteContent(contentStr) 419 + if title == "" { 420 + title = strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath)) 421 + } 422 + 423 + note := &models.Note{ 424 + Title: title, 425 + Content: noteContent, 426 + Tags: tags, 427 + FilePath: filePath, 428 + } 429 + 430 + noteID, err := h.repos.Notes.Create(ctx, note) 431 + if err != nil { 432 + return 0, fmt.Errorf("failed to create note: %w", err) 433 + } 434 + 435 + ui.Infoln("Created note from file: %s", filePath) 436 + ui.Infoln(" Note: %s (ID: %d)", title, noteID) 437 + if len(tags) > 0 { 438 + ui.Infoln(" Tags: %s", strings.Join(tags, ", ")) 439 + } 440 + 441 + return noteID, nil 442 + } 443 + 444 + // parseNoteContent extracts title, content, and tags from markdown content 445 + func parseNoteContent(content string) (title, noteContent string, tags []string) { 446 + lines := strings.Split(content, "\n") 447 + 448 + for _, line := range lines { 449 + line = strings.TrimSpace(line) 450 + if after, ok := strings.CutPrefix(line, "# "); ok { 451 + title = after 452 + break 453 + } 454 + } 455 + 456 + for _, line := range lines { 457 + line = strings.TrimSpace(line) 458 + if strings.HasPrefix(line, "<!-- Tags:") && strings.HasSuffix(line, "-->") { 459 + tagStr := strings.TrimPrefix(line, "<!-- Tags:") 460 + tagStr = strings.TrimSuffix(tagStr, "-->") 461 + tagStr = strings.TrimSpace(tagStr) 462 + 463 + if tagStr != "" { 464 + for tag := range strings.SplitSeq(tagStr, ",") { 465 + tag = strings.TrimSpace(tag) 466 + if tag != "" { 467 + tags = append(tags, tag) 468 + } 469 + } 470 + } 471 + } 472 + } 473 + 474 + noteContent = content 475 + 476 + return title, noteContent, tags 477 + } 478 + 479 + // PushFromFiles creates notes from files and pushes them to leaflet 480 + func (h *PublicationHandler) PushFromFiles(ctx context.Context, filePaths []string, isDraft bool, dryRun bool) error { 481 + if len(filePaths) == 0 { 482 + return fmt.Errorf("no file paths provided") 483 + } 484 + 485 + ui.Infoln("Creating notes from %d file(s)...\n", len(filePaths)) 486 + 487 + noteIDs := make([]int64, 0, len(filePaths)) 488 + var failed int 489 + 490 + for _, filePath := range filePaths { 491 + noteID, err := h.createNoteFromFile(ctx, filePath) 492 + if err != nil { 493 + ui.Warningln("Failed to create note from %s: %v", filePath, err) 494 + failed++ 495 + continue 496 + } 497 + noteIDs = append(noteIDs, noteID) 498 + } 499 + 500 + if len(noteIDs) == 0 { 501 + return fmt.Errorf("failed to create any notes from files") 502 + } 503 + 504 + ui.Newline() 505 + if dryRun { 506 + ui.Successln("Created %d note(s) from files. Skipping leaflet push (dry run).", len(noteIDs)) 507 + ui.Infoln("Note IDs: %v", noteIDs) 508 + return nil 509 + } 510 + 511 + return h.Push(ctx, noteIDs, isDraft, dryRun) 512 + } 513 + 385 514 // Push creates or updates multiple documents on leaflet from local notes 386 - func (h *PublicationHandler) Push(ctx context.Context, noteIDs []int64, isDraft bool) error { 387 - if !h.atproto.IsAuthenticated() { 515 + func (h *PublicationHandler) Push(ctx context.Context, noteIDs []int64, isDraft bool, dryRun bool) error { 516 + if !dryRun && !h.atproto.IsAuthenticated() { 388 517 return fmt.Errorf("not authenticated - run 'noteleaf pub auth' first") 389 518 } 390 519 ··· 392 521 return fmt.Errorf("no note IDs provided") 393 522 } 394 523 395 - ui.Infoln("Processing %d note(s)...\n", len(noteIDs)) 524 + if dryRun { 525 + ui.Infoln("Dry run: validating %d note(s)...\n", len(noteIDs)) 526 + } else { 527 + ui.Infoln("Processing %d note(s)...\n", len(noteIDs)) 528 + } 396 529 397 530 var created, updated, failed int 398 531 var errors []string ··· 406 539 continue 407 540 } 408 541 409 - if note.HasLeafletAssociation() { 410 - err = h.Patch(ctx, noteID) 542 + if dryRun { 543 + _, _, err := h.prepareDocumentForPublish(ctx, noteID, isDraft, note.HasLeafletAssociation()) 411 544 if err != nil { 412 - ui.Warningln(" [%d] Failed to update '%s': %v", noteID, note.Title, err) 545 + ui.Warningln(" [%d] Validation failed for '%s': %v", noteID, note.Title, err) 413 546 errors = append(errors, fmt.Sprintf("note %d (%s): %v", noteID, note.Title, err)) 414 547 failed++ 415 548 } else { 416 - updated++ 549 + ui.Infoln(" [%d] '%s' - validation passed", noteID, note.Title) 550 + if note.HasLeafletAssociation() { 551 + updated++ 552 + } else { 553 + created++ 554 + } 417 555 } 418 556 } else { 419 - err = h.Post(ctx, noteID, isDraft) 420 - if err != nil { 421 - ui.Warningln(" [%d] Failed to create '%s': %v", noteID, note.Title, err) 422 - errors = append(errors, fmt.Sprintf("note %d (%s): %v", noteID, note.Title, err)) 423 - failed++ 557 + if note.HasLeafletAssociation() { 558 + err = h.Patch(ctx, noteID) 559 + if err != nil { 560 + ui.Warningln(" [%d] Failed to update '%s': %v", noteID, note.Title, err) 561 + errors = append(errors, fmt.Sprintf("note %d (%s): %v", noteID, note.Title, err)) 562 + failed++ 563 + } else { 564 + updated++ 565 + } 424 566 } else { 425 - created++ 567 + err = h.Post(ctx, noteID, isDraft) 568 + if err != nil { 569 + ui.Warningln(" [%d] Failed to create '%s': %v", noteID, note.Title, err) 570 + errors = append(errors, fmt.Sprintf("note %d (%s): %v", noteID, note.Title, err)) 571 + failed++ 572 + } else { 573 + created++ 574 + } 426 575 } 427 576 } 428 577 } 429 578 430 579 ui.Newline() 431 - ui.Successln("Push complete: %d created, %d updated, %d failed", created, updated, failed) 580 + if dryRun { 581 + ui.Successln("Dry run complete: %d would be created, %d would be updated, %d failed validation", created, updated, failed) 582 + ui.Infoln("No changes made to leaflet") 583 + } else { 584 + ui.Successln("Push complete: %d created, %d updated, %d failed", created, updated, failed) 585 + } 432 586 433 587 if len(errors) > 0 { 434 588 return fmt.Errorf("push completed with %d error(s)", failed) ··· 525 679 return nil, nil, fmt.Errorf("failed to convert markdown to leaflet format: %w", err) 526 680 } 527 681 682 + publicationURI, err := h.atproto.GetDefaultPublication(ctx) 683 + if err != nil { 684 + return nil, nil, fmt.Errorf("failed to get publication: %w", err) 685 + } 686 + 687 + docType := public.TypeDocument 688 + if isDraft { 689 + docType = public.TypeDocumentDraft 690 + } 691 + 528 692 doc := &public.Document{ 693 + Type: docType, 529 694 Author: session.DID, 530 695 Title: note.Title, 531 696 Description: "", 697 + Publication: publicationURI, 532 698 Pages: []public.LinearDocument{ 533 699 { 534 700 Type: public.TypeLinearDocument, ··· 549 715 return note, doc, nil 550 716 } 551 717 718 + // writeDocumentOutput writes document to a file in JSON or plaintext format 719 + func writeDocumentOutput(doc *public.Document, note *models.Note, outputPath string, plaintext bool) error { 720 + var content []byte 721 + var err error 722 + 723 + if plaintext { 724 + status := "published" 725 + if note != nil && note.IsDraft { 726 + status = "draft" 727 + } 728 + 729 + output := "Document Preview\n" 730 + output += "================\n\n" 731 + output += fmt.Sprintf("Title: %s\n", doc.Title) 732 + output += fmt.Sprintf("Status: %s\n", status) 733 + if note != nil { 734 + output += fmt.Sprintf("Note ID: %d\n", note.ID) 735 + if note.LeafletRKey != nil { 736 + output += fmt.Sprintf("RKey: %s\n", *note.LeafletRKey) 737 + } 738 + } 739 + output += fmt.Sprintf("Pages: %d\n", len(doc.Pages)) 740 + if len(doc.Pages) > 0 { 741 + output += fmt.Sprintf("Blocks: %d\n", len(doc.Pages[0].Blocks)) 742 + } 743 + if doc.PublishedAt != "" { 744 + output += fmt.Sprintf("PublishedAt: %s\n", doc.PublishedAt) 745 + } 746 + if doc.Author != "" { 747 + output += fmt.Sprintf("Author: %s\n", doc.Author) 748 + } 749 + 750 + content = []byte(output) 751 + } else { 752 + content, err = json.MarshalIndent(doc, "", " ") 753 + if err != nil { 754 + return fmt.Errorf("failed to marshal document to JSON: %w", err) 755 + } 756 + } 757 + 758 + if err := os.WriteFile(outputPath, content, 0644); err != nil { 759 + return fmt.Errorf("failed to write output to %s: %w", outputPath, err) 760 + } 761 + 762 + return nil 763 + } 764 + 552 765 // PostPreview shows what would be posted without actually posting 553 - func (h *PublicationHandler) PostPreview(ctx context.Context, noteID int64, isDraft bool) error { 766 + func (h *PublicationHandler) PostPreview(ctx context.Context, noteID int64, isDraft bool, outputPath string, plaintext bool) error { 554 767 if !h.atproto.IsAuthenticated() { 555 768 return fmt.Errorf("not authenticated - run 'noteleaf pub auth' first") 556 769 } ··· 574 787 ui.Infoln(" PublishedAt: %s", doc.PublishedAt) 575 788 } 576 789 ui.Infoln(" Note ID: %d", note.ID) 790 + 791 + if outputPath != "" { 792 + if err := writeDocumentOutput(doc, note, outputPath, plaintext); err != nil { 793 + return err 794 + } 795 + format := "JSON" 796 + if plaintext { 797 + format = "plaintext" 798 + } 799 + ui.Successln("Output written to %s (%s format)", outputPath, format) 800 + } 801 + 577 802 ui.Successln("Preview complete - no changes made") 578 803 579 804 return nil 580 805 } 581 806 582 807 // PostValidate validates markdown conversion without posting 583 - func (h *PublicationHandler) PostValidate(ctx context.Context, noteID int64, isDraft bool) error { 808 + func (h *PublicationHandler) PostValidate(ctx context.Context, noteID int64, isDraft bool, outputPath string, plaintext bool) error { 584 809 if !h.atproto.IsAuthenticated() { 585 810 return fmt.Errorf("not authenticated - run 'noteleaf pub auth' first") 586 811 } ··· 595 820 ui.Infoln(" Title: %s", doc.Title) 596 821 ui.Infoln(" Blocks converted: %d", len(doc.Pages[0].Blocks)) 597 822 823 + if outputPath != "" { 824 + if err := writeDocumentOutput(doc, note, outputPath, plaintext); err != nil { 825 + return err 826 + } 827 + format := "JSON" 828 + if plaintext { 829 + format = "plaintext" 830 + } 831 + ui.Successln("Output written to %s (%s format)", outputPath, format) 832 + } 833 + 598 834 return nil 599 835 } 600 836 601 837 // PatchPreview shows what would be patched without actually patching 602 - func (h *PublicationHandler) PatchPreview(ctx context.Context, noteID int64) error { 838 + func (h *PublicationHandler) PatchPreview(ctx context.Context, noteID int64, outputPath string, plaintext bool) error { 603 839 if !h.atproto.IsAuthenticated() { 604 840 return fmt.Errorf("not authenticated - run 'noteleaf pub auth' first") 605 841 } ··· 628 864 if doc.PublishedAt != "" { 629 865 ui.Infoln(" PublishedAt: %s", doc.PublishedAt) 630 866 } 867 + 868 + if outputPath != "" { 869 + if err := writeDocumentOutput(doc, note, outputPath, plaintext); err != nil { 870 + return err 871 + } 872 + format := "JSON" 873 + if plaintext { 874 + format = "plaintext" 875 + } 876 + ui.Successln("Output written to %s (%s format)", outputPath, format) 877 + } 878 + 631 879 ui.Successln("Preview complete - no changes made") 632 880 633 881 return nil 634 882 } 635 883 636 884 // PatchValidate validates markdown conversion without patching 637 - func (h *PublicationHandler) PatchValidate(ctx context.Context, noteID int64) error { 885 + func (h *PublicationHandler) PatchValidate(ctx context.Context, noteID int64, outputPath string, plaintext bool) error { 638 886 if !h.atproto.IsAuthenticated() { 639 887 return fmt.Errorf("not authenticated - run 'noteleaf pub auth' first") 640 888 } ··· 655 903 ui.Infoln(" RKey: %s", *note.LeafletRKey) 656 904 ui.Infoln(" Blocks converted: %d", len(doc.Pages[0].Blocks)) 657 905 906 + if outputPath != "" { 907 + if err := writeDocumentOutput(doc, note, outputPath, plaintext); err != nil { 908 + return err 909 + } 910 + format := "JSON" 911 + if plaintext { 912 + format = "plaintext" 913 + } 914 + ui.Successln("Output written to %s (%s format)", outputPath, format) 915 + } 916 + 658 917 return nil 659 918 } 660 919 ··· 668 927 return "Authenticated (session details unavailable)" 669 928 } 670 929 return "Not authenticated" 930 + } 931 + 932 + // GetLastAuthenticatedHandle returns the last authenticated handle from config 933 + func (h *PublicationHandler) GetLastAuthenticatedHandle() string { 934 + if h.config != nil && h.config.ATProtoHandle != "" { 935 + return h.config.ATProtoHandle 936 + } 937 + return "" 671 938 } 672 939 673 940 // extractNoteDirectory extracts the directory path from a note's FilePath
+98 -51
internal/handlers/publication_test.go
··· 189 189 }) 190 190 }) 191 191 192 + t.Run("GetLastAuthenticatedHandle", func(t *testing.T) { 193 + t.Run("returns empty string when no config", func(t *testing.T) { 194 + handler := &PublicationHandler{ 195 + config: nil, 196 + } 197 + 198 + handle := handler.GetLastAuthenticatedHandle() 199 + if handle != "" { 200 + t.Errorf("Expected empty string, got '%s'", handle) 201 + } 202 + }) 203 + 204 + t.Run("returns empty string when handle not set", func(t *testing.T) { 205 + handler := &PublicationHandler{ 206 + config: &store.Config{}, 207 + } 208 + 209 + handle := handler.GetLastAuthenticatedHandle() 210 + if handle != "" { 211 + t.Errorf("Expected empty string, got '%s'", handle) 212 + } 213 + }) 214 + 215 + t.Run("returns handle from config", func(t *testing.T) { 216 + expectedHandle := "test.bsky.social" 217 + handler := &PublicationHandler{ 218 + config: &store.Config{ 219 + ATProtoHandle: expectedHandle, 220 + }, 221 + } 222 + 223 + handle := handler.GetLastAuthenticatedHandle() 224 + if handle != expectedHandle { 225 + t.Errorf("Expected '%s', got '%s'", expectedHandle, handle) 226 + } 227 + }) 228 + 229 + t.Run("returns handle after successful authentication", func(t *testing.T) { 230 + suite := NewHandlerTestSuite(t) 231 + defer suite.Cleanup() 232 + 233 + handler := CreateHandler(t, NewPublicationHandler) 234 + ctx := context.Background() 235 + 236 + mock := services.SetupSuccessfulAuthMocks() 237 + handler.atproto = mock 238 + 239 + err := handler.Auth(ctx, "user.bsky.social", "password123") 240 + suite.AssertNoError(err, "authentication should succeed") 241 + 242 + handle := handler.GetLastAuthenticatedHandle() 243 + if handle != "user.bsky.social" { 244 + t.Errorf("Expected 'user.bsky.social', got '%s'", handle) 245 + } 246 + }) 247 + }) 248 + 192 249 t.Run("NewPublicationHandler", func(t *testing.T) { 193 250 t.Run("creates handler successfully", func(t *testing.T) { 194 251 suite := NewHandlerTestSuite(t) ··· 1019 1076 handler := CreateHandler(t, NewPublicationHandler) 1020 1077 ctx := context.Background() 1021 1078 1022 - err := handler.PostPreview(ctx, 1, false) 1079 + err := handler.PostPreview(ctx, 1, false, "", false) 1023 1080 if err == nil { 1024 1081 t.Error("Expected error when not authenticated") 1025 1082 } ··· 1051 1108 t.Fatalf("Failed to restore session: %v", err) 1052 1109 } 1053 1110 1054 - err = handler.PostPreview(ctx, 999, false) 1111 + err = handler.PostPreview(ctx, 999, false, "", false) 1055 1112 if err == nil { 1056 1113 t.Error("Expected error when note does not exist") 1057 1114 } ··· 1095 1152 t.Fatalf("Failed to restore session: %v", err) 1096 1153 } 1097 1154 1098 - err = handler.PostPreview(ctx, id, false) 1155 + err = handler.PostPreview(ctx, id, false, "", false) 1099 1156 if err == nil { 1100 1157 t.Error("Expected error when note already published") 1101 1158 } ··· 1120 1177 id, err := handler.repos.Notes.Create(ctx, note) 1121 1178 suite.AssertNoError(err, "create note") 1122 1179 1123 - session := &services.Session{ 1180 + mock := services.NewMockATProtoService() 1181 + mock.IsAuthenticatedVal = true 1182 + mock.Session = &services.Session{ 1124 1183 DID: "did:plc:test123", 1125 1184 Handle: "test.bsky.social", 1126 1185 AccessJWT: "access_token", ··· 1129 1188 ExpiresAt: time.Now().Add(2 * time.Hour), 1130 1189 Authenticated: true, 1131 1190 } 1132 - 1133 - err = handler.atproto.RestoreSession(session) 1134 - if err != nil { 1135 - t.Fatalf("Failed to restore session: %v", err) 1136 - } 1191 + handler.atproto = mock 1137 1192 1138 - err = handler.PostPreview(ctx, id, false) 1193 + err = handler.PostPreview(ctx, id, false, "", false) 1139 1194 suite.AssertNoError(err, "preview should succeed") 1140 1195 }) 1141 1196 ··· 1154 1209 id, err := handler.repos.Notes.Create(ctx, note) 1155 1210 suite.AssertNoError(err, "create note") 1156 1211 1157 - session := &services.Session{ 1212 + mock := services.NewMockATProtoService() 1213 + mock.IsAuthenticatedVal = true 1214 + mock.Session = &services.Session{ 1158 1215 DID: "did:plc:test123", 1159 1216 Handle: "test.bsky.social", 1160 1217 AccessJWT: "access_token", ··· 1163 1220 ExpiresAt: time.Now().Add(2 * time.Hour), 1164 1221 Authenticated: true, 1165 1222 } 1223 + handler.atproto = mock 1166 1224 1167 - err = handler.atproto.RestoreSession(session) 1168 - if err != nil { 1169 - t.Fatalf("Failed to restore session: %v", err) 1170 - } 1171 - 1172 - err = handler.PostPreview(ctx, id, true) 1225 + err = handler.PostPreview(ctx, id, true, "", false) 1173 1226 suite.AssertNoError(err, "preview draft should succeed") 1174 1227 }) 1175 1228 }) ··· 1182 1235 handler := CreateHandler(t, NewPublicationHandler) 1183 1236 ctx := context.Background() 1184 1237 1185 - err := handler.PostValidate(ctx, 1, false) 1238 + err := handler.PostValidate(ctx, 1, false, "", false) 1186 1239 if err == nil { 1187 1240 t.Error("Expected error when not authenticated") 1188 1241 } ··· 1207 1260 id, err := handler.repos.Notes.Create(ctx, note) 1208 1261 suite.AssertNoError(err, "create note") 1209 1262 1210 - session := &services.Session{ 1263 + mock := services.NewMockATProtoService() 1264 + mock.IsAuthenticatedVal = true 1265 + mock.Session = &services.Session{ 1211 1266 DID: "did:plc:test123", 1212 1267 Handle: "test.bsky.social", 1213 1268 AccessJWT: "access_token", ··· 1216 1271 ExpiresAt: time.Now().Add(2 * time.Hour), 1217 1272 Authenticated: true, 1218 1273 } 1274 + handler.atproto = mock 1219 1275 1220 - err = handler.atproto.RestoreSession(session) 1221 - if err != nil { 1222 - t.Fatalf("Failed to restore session: %v", err) 1223 - } 1224 - 1225 - err = handler.PostValidate(ctx, id, false) 1276 + err = handler.PostValidate(ctx, id, false, "", false) 1226 1277 suite.AssertNoError(err, "validation should succeed") 1227 1278 }) 1228 1279 }) ··· 1235 1286 handler := CreateHandler(t, NewPublicationHandler) 1236 1287 ctx := context.Background() 1237 1288 1238 - err := handler.PatchPreview(ctx, 1) 1289 + err := handler.PatchPreview(ctx, 1, "", false) 1239 1290 if err == nil { 1240 1291 t.Error("Expected error when not authenticated") 1241 1292 } ··· 1267 1318 t.Fatalf("Failed to restore session: %v", err) 1268 1319 } 1269 1320 1270 - err = handler.PatchPreview(ctx, 999) 1321 + err = handler.PatchPreview(ctx, 999, "", false) 1271 1322 if err == nil { 1272 1323 t.Error("Expected error when note does not exist") 1273 1324 } ··· 1307 1358 t.Fatalf("Failed to restore session: %v", err) 1308 1359 } 1309 1360 1310 - err = handler.PatchPreview(ctx, id) 1361 + err = handler.PatchPreview(ctx, id, "", false) 1311 1362 if err == nil { 1312 1363 t.Error("Expected error when note not published") 1313 1364 } ··· 1339 1390 id, err := handler.repos.Notes.Create(ctx, note) 1340 1391 suite.AssertNoError(err, "create note") 1341 1392 1342 - session := &services.Session{ 1393 + mock := services.NewMockATProtoService() 1394 + mock.IsAuthenticatedVal = true 1395 + mock.Session = &services.Session{ 1343 1396 DID: "did:plc:test123", 1344 1397 Handle: "test.bsky.social", 1345 1398 AccessJWT: "access_token", ··· 1348 1401 ExpiresAt: time.Now().Add(2 * time.Hour), 1349 1402 Authenticated: true, 1350 1403 } 1351 - 1352 - err = handler.atproto.RestoreSession(session) 1353 - if err != nil { 1354 - t.Fatalf("Failed to restore session: %v", err) 1355 - } 1404 + handler.atproto = mock 1356 1405 1357 - err = handler.PatchPreview(ctx, id) 1406 + err = handler.PatchPreview(ctx, id, "", false) 1358 1407 suite.AssertNoError(err, "preview should succeed") 1359 1408 }) 1360 1409 }) ··· 1367 1416 handler := CreateHandler(t, NewPublicationHandler) 1368 1417 ctx := context.Background() 1369 1418 1370 - err := handler.PatchValidate(ctx, 1) 1419 + err := handler.PatchValidate(ctx, 1, "", false) 1371 1420 if err == nil { 1372 1421 t.Error("Expected error when not authenticated") 1373 1422 } ··· 1397 1446 id, err := handler.repos.Notes.Create(ctx, note) 1398 1447 suite.AssertNoError(err, "create note") 1399 1448 1400 - session := &services.Session{ 1449 + mock := services.NewMockATProtoService() 1450 + mock.IsAuthenticatedVal = true 1451 + mock.Session = &services.Session{ 1401 1452 DID: "did:plc:test123", 1402 1453 Handle: "test.bsky.social", 1403 1454 AccessJWT: "access_token", ··· 1406 1457 ExpiresAt: time.Now().Add(2 * time.Hour), 1407 1458 Authenticated: true, 1408 1459 } 1460 + handler.atproto = mock 1409 1461 1410 - err = handler.atproto.RestoreSession(session) 1411 - if err != nil { 1412 - t.Fatalf("Failed to restore session: %v", err) 1413 - } 1414 - 1415 - err = handler.PatchValidate(ctx, id) 1462 + err = handler.PatchValidate(ctx, id, "", false) 1416 1463 suite.AssertNoError(err, "validation should succeed") 1417 1464 }) 1418 1465 }) ··· 1662 1709 handler := CreateHandler(t, NewPublicationHandler) 1663 1710 ctx := context.Background() 1664 1711 1665 - err := handler.Push(ctx, []int64{1, 2, 3}, false) 1712 + err := handler.Push(ctx, []int64{1, 2, 3}, false, false) 1666 1713 if err == nil { 1667 1714 t.Error("Expected error when not authenticated") 1668 1715 } ··· 1694 1741 t.Fatalf("Failed to restore session: %v", err) 1695 1742 } 1696 1743 1697 - err = handler.Push(ctx, []int64{}, false) 1744 + err = handler.Push(ctx, []int64{}, false, false) 1698 1745 if err == nil { 1699 1746 t.Error("Expected error when no note IDs provided") 1700 1747 } ··· 1726 1773 t.Fatalf("Failed to restore session: %v", err) 1727 1774 } 1728 1775 1729 - err = handler.Push(ctx, []int64{999}, false) 1776 + err = handler.Push(ctx, []int64{999}, false, false) 1730 1777 if err == nil { 1731 1778 t.Error("Expected error when note not found") 1732 1779 } ··· 1773 1820 t.Fatalf("Failed to restore session: %v", err) 1774 1821 } 1775 1822 1776 - err = handler.Push(ctx, []int64{id1, id2}, false) 1823 + err = handler.Push(ctx, []int64{id1, id2}, false, false) 1777 1824 1778 1825 if err != nil && !strings.Contains(err.Error(), "not authenticated") { 1779 1826 t.Logf("Got error during push (expected for external service call): %v", err) ··· 1830 1877 t.Fatalf("Failed to restore session: %v", err) 1831 1878 } 1832 1879 1833 - err = handler.Push(ctx, []int64{id1, id2}, false) 1880 + err = handler.Push(ctx, []int64{id1, id2}, false, false) 1834 1881 1835 1882 if err != nil && !strings.Contains(err.Error(), "not authenticated") { 1836 1883 t.Logf("Got error during push (expected for external service call): %v", err) ··· 1882 1929 t.Fatalf("Failed to restore session: %v", err) 1883 1930 } 1884 1931 1885 - err = handler.Push(ctx, []int64{newID, existingID}, false) 1932 + err = handler.Push(ctx, []int64{newID, existingID}, false, false) 1886 1933 1887 1934 if err != nil && !strings.Contains(err.Error(), "not authenticated") { 1888 1935 t.Logf("Got error during push (expected for external service call): %v", err) ··· 1920 1967 } 1921 1968 1922 1969 invalidID := int64(999) 1923 - err = handler.Push(ctx, []int64{id1, invalidID}, false) 1970 + err = handler.Push(ctx, []int64{id1, invalidID}, false, false) 1924 1971 1925 1972 if err == nil { 1926 1973 t.Error("Expected error due to invalid note ID") ··· 1961 2008 t.Fatalf("Failed to restore session: %v", err) 1962 2009 } 1963 2010 1964 - err = handler.Push(ctx, []int64{id}, true) 2011 + err = handler.Push(ctx, []int64{id}, true, false) 1965 2012 1966 2013 if err != nil && !strings.Contains(err.Error(), "not authenticated") { 1967 2014 t.Logf("Got error during push (expected for external service call): %v", err)
+7 -1
internal/handlers/task_helpers.go
··· 18 18 Context string 19 19 Tags []string 20 20 Due string 21 + Wait string 22 + Scheduled string 21 23 Recur string 22 24 Until string 23 25 ParentUUID string ··· 25 27 } 26 28 27 29 // parseDescription extracts inline metadata from description text 28 - // Supports: +project @context #tag due:YYYY-MM-DD recur:RULE until:DATE parent:UUID depends:UUID1,UUID2 30 + // Supports: +project @context #tag due:YYYY-MM-DD wait:YYYY-MM-DD scheduled:YYYY-MM-DD recur:RULE until:DATE parent:UUID depends:UUID1,UUID2 29 31 func parseDescription(text string) *ParsedTaskData { 30 32 parsed := &ParsedTaskData{Tags: []string{}, DependsOn: []string{}} 31 33 words := strings.Fields(text) ··· 41 43 parsed.Tags = append(parsed.Tags, strings.TrimPrefix(word, "#")) 42 44 case strings.HasPrefix(word, "due:"): 43 45 parsed.Due = strings.TrimPrefix(word, "due:") 46 + case strings.HasPrefix(word, "wait:"): 47 + parsed.Wait = strings.TrimPrefix(word, "wait:") 48 + case strings.HasPrefix(word, "scheduled:"): 49 + parsed.Scheduled = strings.TrimPrefix(word, "scheduled:") 44 50 case strings.HasPrefix(word, "recur:"): 45 51 parsed.Recur = strings.TrimPrefix(word, "recur:") 46 52 case strings.HasPrefix(word, "until:"):
+526 -5
internal/handlers/tasks.go
··· 6 6 "fmt" 7 7 "os" 8 8 "slices" 9 + "sort" 9 10 "strconv" 10 11 "strings" 11 12 "time" ··· 51 52 } 52 53 53 54 // Create creates a new task 54 - func (h *TaskHandler) Create(ctx context.Context, description, priority, project, context, due, recur, until, parentUUID, dependsOn string, tags []string) error { 55 + func (h *TaskHandler) Create(ctx context.Context, description, priority, project, context, due, wait, scheduled, recur, until, parentUUID, dependsOn string, tags []string) error { 55 56 if description == "" { 56 57 return fmt.Errorf("task description required") 57 58 } ··· 67 68 if due != "" { 68 69 parsed.Due = due 69 70 } 71 + if wait != "" { 72 + parsed.Wait = wait 73 + } 74 + if scheduled != "" { 75 + parsed.Scheduled = scheduled 76 + } 70 77 if recur != "" { 71 78 parsed.Recur = recur 72 79 } ··· 100 107 task.Due = &dueTime 101 108 } else { 102 109 return fmt.Errorf("invalid due date format, use YYYY-MM-DD: %w", err) 110 + } 111 + } 112 + 113 + if parsed.Wait != "" { 114 + if waitTime, err := time.Parse("2006-01-02", parsed.Wait); err == nil { 115 + task.Wait = &waitTime 116 + } else { 117 + return fmt.Errorf("invalid wait date format, use YYYY-MM-DD: %w", err) 118 + } 119 + } 120 + 121 + if parsed.Scheduled != "" { 122 + if scheduledTime, err := time.Parse("2006-01-02", parsed.Scheduled); err == nil { 123 + task.Scheduled = &scheduledTime 124 + } else { 125 + return fmt.Errorf("invalid scheduled date format, use YYYY-MM-DD: %w", err) 103 126 } 104 127 } 105 128 ··· 154 177 } 155 178 156 179 // List lists all tasks with optional filtering 157 - func (h *TaskHandler) List(ctx context.Context, static, showAll bool, status, priority, project, context string) error { 180 + func (h *TaskHandler) List(ctx context.Context, static, showAll bool, status, priority, project, context, sortBy string) error { 158 181 if static { 159 - return h.listTasksStatic(ctx, showAll, status, priority, project, context) 182 + return h.listTasksStatic(ctx, showAll, status, priority, project, context, sortBy) 160 183 } 161 184 162 185 return h.listTasksInteractive(ctx, showAll, status, priority, project, context) 163 186 } 164 187 165 - func (h *TaskHandler) listTasksStatic(ctx context.Context, showAll bool, status, priority, project, context string) error { 188 + func (h *TaskHandler) listTasksStatic(ctx context.Context, showAll bool, status, priority, project, context, sortBy string) error { 166 189 opts := repo.TaskListOptions{ 167 190 Status: status, 168 191 Priority: priority, ··· 179 202 return fmt.Errorf("failed to list tasks: %w", err) 180 203 } 181 204 205 + if sortBy == "urgency" { 206 + now := time.Now() 207 + sort.Slice(tasks, func(i, j int) bool { 208 + return tasks[i].Urgency(now) > tasks[j].Urgency(now) 209 + }) 210 + } 211 + 182 212 if len(tasks) == 0 { 183 213 fmt.Printf("No tasks found matching criteria\n") 184 214 return nil 185 215 } 186 216 187 - fmt.Printf("Found %d task(s):\n\n", len(tasks)) 217 + fmt.Printf("Found %d task(s)", len(tasks)) 218 + if sortBy == "urgency" { 219 + fmt.Printf(" (sorted by urgency)") 220 + } 221 + fmt.Printf(":\n\n") 222 + 188 223 for _, task := range tasks { 224 + if sortBy == "urgency" { 225 + urgency := task.Urgency(time.Now()) 226 + fmt.Printf("[%.1f] ", urgency) 227 + } 189 228 printTask(task) 190 229 } 191 230 ··· 958 997 959 998 return nil 960 999 } 1000 + 1001 + // NextActions shows actionable tasks sorted by urgency 1002 + func (h *TaskHandler) NextActions(ctx context.Context, limit int) error { 1003 + opts := repo.TaskListOptions{ 1004 + SortBy: "urgency", 1005 + SortOrder: "desc", 1006 + } 1007 + 1008 + tasks, err := h.repos.Tasks.List(ctx, opts) 1009 + if err != nil { 1010 + return fmt.Errorf("failed to list tasks: %w", err) 1011 + } 1012 + 1013 + now := time.Now() 1014 + var actionable []*models.Task 1015 + for _, task := range tasks { 1016 + if task.IsActionable(now) { 1017 + actionable = append(actionable, task) 1018 + } 1019 + } 1020 + 1021 + sort.Slice(actionable, func(i, j int) bool { 1022 + return actionable[i].Urgency(now) > actionable[j].Urgency(now) 1023 + }) 1024 + 1025 + if limit > 0 && len(actionable) > limit { 1026 + actionable = actionable[:limit] 1027 + } 1028 + 1029 + if len(actionable) == 0 { 1030 + fmt.Println("No actionable tasks found") 1031 + return nil 1032 + } 1033 + 1034 + fmt.Printf("Next Actions (%d tasks, sorted by urgency):\n\n", len(actionable)) 1035 + for i, task := range actionable { 1036 + urgency := task.Urgency(now) 1037 + fmt.Printf("%d. [Urgency: %.1f] ", i+1, urgency) 1038 + printTask(task) 1039 + } 1040 + 1041 + return nil 1042 + } 1043 + 1044 + // ReportCompleted shows completed tasks 1045 + func (h *TaskHandler) ReportCompleted(ctx context.Context, limit int) error { 1046 + opts := repo.TaskListOptions{ 1047 + Status: "done", 1048 + SortBy: "modified", 1049 + SortOrder: "desc", 1050 + Limit: limit, 1051 + } 1052 + 1053 + tasks, err := h.repos.Tasks.List(ctx, opts) 1054 + if err != nil { 1055 + return fmt.Errorf("failed to list completed tasks: %w", err) 1056 + } 1057 + 1058 + if len(tasks) == 0 { 1059 + fmt.Println("No completed tasks found") 1060 + return nil 1061 + } 1062 + 1063 + fmt.Printf("Completed Tasks (%d):\n\n", len(tasks)) 1064 + for _, task := range tasks { 1065 + fmt.Printf(" ") 1066 + printTask(task) 1067 + if task.End != nil { 1068 + fmt.Printf(" Completed: %s\n", task.End.Format("2006-01-02 15:04")) 1069 + } 1070 + } 1071 + 1072 + return nil 1073 + } 1074 + 1075 + // ReportWaiting shows tasks that are waiting 1076 + func (h *TaskHandler) ReportWaiting(ctx context.Context) error { 1077 + opts := repo.TaskListOptions{ 1078 + SortBy: "wait", 1079 + SortOrder: "asc", 1080 + } 1081 + 1082 + tasks, err := h.repos.Tasks.List(ctx, opts) 1083 + if err != nil { 1084 + return fmt.Errorf("failed to list tasks: %w", err) 1085 + } 1086 + 1087 + now := time.Now() 1088 + var waiting []*models.Task 1089 + for _, task := range tasks { 1090 + if task.IsWaiting(now) { 1091 + waiting = append(waiting, task) 1092 + } 1093 + } 1094 + 1095 + if len(waiting) == 0 { 1096 + fmt.Println("No waiting tasks found") 1097 + return nil 1098 + } 1099 + 1100 + fmt.Printf("Waiting Tasks (%d):\n\n", len(waiting)) 1101 + for _, task := range waiting { 1102 + fmt.Printf(" ") 1103 + printTask(task) 1104 + if task.Wait != nil { 1105 + daysUntil := int(task.Wait.Sub(now).Hours() / 24) 1106 + fmt.Printf(" Wait until: %s (%d days)\n", task.Wait.Format("2006-01-02"), daysUntil) 1107 + } 1108 + } 1109 + 1110 + return nil 1111 + } 1112 + 1113 + // ReportBlocked shows blocked tasks 1114 + func (h *TaskHandler) ReportBlocked(ctx context.Context) error { 1115 + opts := repo.TaskListOptions{ 1116 + Status: "blocked", 1117 + } 1118 + 1119 + tasks, err := h.repos.Tasks.List(ctx, opts) 1120 + if err != nil { 1121 + return fmt.Errorf("failed to list blocked tasks: %w", err) 1122 + } 1123 + 1124 + if len(tasks) == 0 { 1125 + fmt.Println("No blocked tasks found") 1126 + return nil 1127 + } 1128 + 1129 + fmt.Printf("Blocked Tasks (%d):\n\n", len(tasks)) 1130 + for _, task := range tasks { 1131 + fmt.Printf(" ") 1132 + printTask(task) 1133 + 1134 + if len(task.DependsOn) > 0 { 1135 + fmt.Printf(" Depends on %d task(s)\n", len(task.DependsOn)) 1136 + } 1137 + } 1138 + 1139 + return nil 1140 + } 1141 + 1142 + // Calendar shows tasks by due date in a calendar-like view 1143 + func (h *TaskHandler) Calendar(ctx context.Context, weeks int) error { 1144 + if weeks <= 0 { 1145 + weeks = 4 1146 + } 1147 + 1148 + now := time.Now() 1149 + startDate := now.Truncate(24 * time.Hour) 1150 + endDate := startDate.AddDate(0, 0, weeks*7) 1151 + 1152 + opts := repo.TaskListOptions{ 1153 + SortBy: "due", 1154 + SortOrder: "asc", 1155 + } 1156 + 1157 + tasks, err := h.repos.Tasks.List(ctx, opts) 1158 + if err != nil { 1159 + return fmt.Errorf("failed to list tasks: %w", err) 1160 + } 1161 + 1162 + tasksByDate := make(map[string][]*models.Task) 1163 + overdue := []*models.Task{} 1164 + 1165 + for _, task := range tasks { 1166 + if task.Due == nil || task.IsCompleted() || task.IsDone() { 1167 + continue 1168 + } 1169 + 1170 + dueDate := task.Due.Truncate(24 * time.Hour) 1171 + if dueDate.Before(startDate) { 1172 + overdue = append(overdue, task) 1173 + } else if dueDate.Before(endDate) { 1174 + dateKey := dueDate.Format("2006-01-02") 1175 + tasksByDate[dateKey] = append(tasksByDate[dateKey], task) 1176 + } 1177 + } 1178 + 1179 + fmt.Printf("Calendar View (Next %d weeks)\n", weeks) 1180 + fmt.Printf("Today: %s\n\n", now.Format("Monday, January 2, 2006")) 1181 + 1182 + if len(overdue) > 0 { 1183 + fmt.Printf("OVERDUE (%d tasks):\n", len(overdue)) 1184 + for _, task := range overdue { 1185 + daysOverdue := int(now.Sub(*task.Due).Hours() / 24) 1186 + fmt.Printf(" [%d days overdue] ", daysOverdue) 1187 + printTask(task) 1188 + } 1189 + fmt.Println() 1190 + } 1191 + 1192 + currentDate := startDate 1193 + for currentDate.Before(endDate) { 1194 + weekStart := currentDate 1195 + weekEnd := currentDate.AddDate(0, 0, 6) 1196 + 1197 + weekTasks := 0 1198 + for d := weekStart; !d.After(weekEnd); d = d.AddDate(0, 0, 1) { 1199 + dateKey := d.Format("2006-01-02") 1200 + weekTasks += len(tasksByDate[dateKey]) 1201 + } 1202 + 1203 + if weekTasks > 0 { 1204 + fmt.Printf("Week of %s (%d tasks):\n", weekStart.Format("Jan 2"), weekTasks) 1205 + 1206 + for d := weekStart; !d.After(weekEnd); d = d.AddDate(0, 0, 1) { 1207 + dateKey := d.Format("2006-01-02") 1208 + dayTasks := tasksByDate[dateKey] 1209 + 1210 + if len(dayTasks) > 0 { 1211 + dayName := d.Format("Monday, Jan 2") 1212 + if d.Format("2006-01-02") == now.Format("2006-01-02") { 1213 + dayName += " (TODAY)" 1214 + } 1215 + fmt.Printf(" %s:\n", dayName) 1216 + for _, task := range dayTasks { 1217 + fmt.Printf(" ") 1218 + printTask(task) 1219 + } 1220 + } 1221 + } 1222 + fmt.Println() 1223 + } 1224 + 1225 + currentDate = currentDate.AddDate(0, 0, 7) 1226 + } 1227 + 1228 + if len(overdue) == 0 && len(tasksByDate) == 0 { 1229 + fmt.Println("No tasks with due dates in the next", weeks, "weeks") 1230 + } 1231 + 1232 + return nil 1233 + } 1234 + 1235 + // Annotate adds an annotation to a task 1236 + func (h *TaskHandler) Annotate(ctx context.Context, taskID, annotation string) error { 1237 + if annotation == "" { 1238 + return fmt.Errorf("annotation text required") 1239 + } 1240 + 1241 + var task *models.Task 1242 + var err error 1243 + 1244 + if id, err_ := strconv.ParseInt(taskID, 10, 64); err_ == nil { 1245 + task, err = h.repos.Tasks.Get(ctx, id) 1246 + } else { 1247 + task, err = h.repos.Tasks.GetByUUID(ctx, taskID) 1248 + } 1249 + 1250 + if err != nil { 1251 + return fmt.Errorf("failed to find task: %w", err) 1252 + } 1253 + 1254 + task.Annotations = append(task.Annotations, annotation) 1255 + 1256 + err = h.repos.Tasks.Update(ctx, task) 1257 + if err != nil { 1258 + return fmt.Errorf("failed to update task: %w", err) 1259 + } 1260 + 1261 + fmt.Printf("Annotation added to task (ID: %d): %s\n", task.ID, task.Description) 1262 + fmt.Printf("Annotation: %s\n", annotation) 1263 + 1264 + return nil 1265 + } 1266 + 1267 + // ListAnnotations lists all annotations for a task 1268 + func (h *TaskHandler) ListAnnotations(ctx context.Context, taskID string) error { 1269 + var task *models.Task 1270 + var err error 1271 + 1272 + if id, err_ := strconv.ParseInt(taskID, 10, 64); err_ == nil { 1273 + task, err = h.repos.Tasks.Get(ctx, id) 1274 + } else { 1275 + task, err = h.repos.Tasks.GetByUUID(ctx, taskID) 1276 + } 1277 + 1278 + if err != nil { 1279 + return fmt.Errorf("failed to find task: %w", err) 1280 + } 1281 + 1282 + fmt.Printf("Task (ID: %d): %s\n", task.ID, task.Description) 1283 + 1284 + if len(task.Annotations) == 0 { 1285 + fmt.Printf("No annotations\n") 1286 + return nil 1287 + } 1288 + 1289 + fmt.Printf("Annotations (%d):\n", len(task.Annotations)) 1290 + for i, annotation := range task.Annotations { 1291 + fmt.Printf(" %d. %s\n", i+1, annotation) 1292 + } 1293 + 1294 + return nil 1295 + } 1296 + 1297 + // RemoveAnnotation removes an annotation from a task by index 1298 + func (h *TaskHandler) RemoveAnnotation(ctx context.Context, taskID string, index int) error { 1299 + var task *models.Task 1300 + var err error 1301 + 1302 + if id, err_ := strconv.ParseInt(taskID, 10, 64); err_ == nil { 1303 + task, err = h.repos.Tasks.Get(ctx, id) 1304 + } else { 1305 + task, err = h.repos.Tasks.GetByUUID(ctx, taskID) 1306 + } 1307 + 1308 + if err != nil { 1309 + return fmt.Errorf("failed to find task: %w", err) 1310 + } 1311 + 1312 + if len(task.Annotations) == 0 { 1313 + return fmt.Errorf("task has no annotations") 1314 + } 1315 + 1316 + if index < 1 || index > len(task.Annotations) { 1317 + return fmt.Errorf("annotation index out of range (1-%d)", len(task.Annotations)) 1318 + } 1319 + 1320 + annotation := task.Annotations[index-1] 1321 + task.Annotations = append(task.Annotations[:index-1], task.Annotations[index:]...) 1322 + 1323 + err = h.repos.Tasks.Update(ctx, task) 1324 + if err != nil { 1325 + return fmt.Errorf("failed to update task: %w", err) 1326 + } 1327 + 1328 + fmt.Printf("Annotation removed from task (ID: %d): %s\n", task.ID, task.Description) 1329 + fmt.Printf("Removed: %s\n", annotation) 1330 + 1331 + return nil 1332 + } 1333 + 1334 + // BulkEdit updates multiple tasks with the same changes 1335 + func (h *TaskHandler) BulkEdit(ctx context.Context, taskIDs []string, status, priority, project, context string, tags []string, addTags, removeTags bool) error { 1336 + if len(taskIDs) == 0 { 1337 + return fmt.Errorf("no task IDs provided") 1338 + } 1339 + 1340 + var ids []int64 1341 + for _, taskID := range taskIDs { 1342 + id, err := strconv.ParseInt(taskID, 10, 64) 1343 + if err != nil { 1344 + task, err := h.repos.Tasks.GetByUUID(ctx, taskID) 1345 + if err != nil { 1346 + return fmt.Errorf("invalid task ID %s: %w", taskID, err) 1347 + } 1348 + id = task.ID 1349 + } 1350 + ids = append(ids, id) 1351 + } 1352 + 1353 + updates := &models.Task{ 1354 + Status: status, 1355 + Priority: priority, 1356 + Project: project, 1357 + Context: context, 1358 + } 1359 + 1360 + if len(tags) > 0 { 1361 + if addTags { 1362 + for _, id := range ids { 1363 + task, err := h.repos.Tasks.Get(ctx, id) 1364 + if err != nil { 1365 + return fmt.Errorf("failed to get task: %w", err) 1366 + } 1367 + for _, tag := range tags { 1368 + if !slices.Contains(task.Tags, tag) { 1369 + task.Tags = append(task.Tags, tag) 1370 + } 1371 + } 1372 + if err := h.repos.Tasks.Update(ctx, task); err != nil { 1373 + return fmt.Errorf("failed to update task: %w", err) 1374 + } 1375 + } 1376 + } else if removeTags { 1377 + for _, id := range ids { 1378 + task, err := h.repos.Tasks.Get(ctx, id) 1379 + if err != nil { 1380 + return fmt.Errorf("failed to get task: %w", err) 1381 + } 1382 + for _, tag := range tags { 1383 + task.Tags = removeString(task.Tags, tag) 1384 + } 1385 + if err := h.repos.Tasks.Update(ctx, task); err != nil { 1386 + return fmt.Errorf("failed to update task: %w", err) 1387 + } 1388 + } 1389 + } else { 1390 + updates.Tags = tags 1391 + } 1392 + } 1393 + 1394 + if err := h.repos.Tasks.BulkUpdate(ctx, ids, updates); err != nil { 1395 + return fmt.Errorf("bulk update failed: %w", err) 1396 + } 1397 + 1398 + fmt.Printf("Updated %d task(s)\n", len(ids)) 1399 + if status != "" { 1400 + fmt.Printf("Status: %s\n", status) 1401 + } 1402 + if priority != "" { 1403 + fmt.Printf("Priority: %s\n", priority) 1404 + } 1405 + if project != "" { 1406 + fmt.Printf("Project: %s\n", project) 1407 + } 1408 + if context != "" { 1409 + fmt.Printf("Context: %s\n", context) 1410 + } 1411 + if len(tags) > 0 { 1412 + if addTags { 1413 + fmt.Printf("Added tags: %s\n", strings.Join(tags, ", ")) 1414 + } else if removeTags { 1415 + fmt.Printf("Removed tags: %s\n", strings.Join(tags, ", ")) 1416 + } else { 1417 + fmt.Printf("Set tags: %s\n", strings.Join(tags, ", ")) 1418 + } 1419 + } 1420 + 1421 + return nil 1422 + } 1423 + 1424 + // UndoTask reverts a task to its previous state 1425 + func (h *TaskHandler) UndoTask(ctx context.Context, taskID string) error { 1426 + var task *models.Task 1427 + var err error 1428 + 1429 + if id, err_ := strconv.ParseInt(taskID, 10, 64); err_ == nil { 1430 + task, err = h.repos.Tasks.Get(ctx, id) 1431 + } else { 1432 + task, err = h.repos.Tasks.GetByUUID(ctx, taskID) 1433 + } 1434 + 1435 + if err != nil { 1436 + return fmt.Errorf("failed to find task: %w", err) 1437 + } 1438 + 1439 + err = h.repos.Tasks.UndoLastChange(ctx, task.ID) 1440 + if err != nil { 1441 + return fmt.Errorf("failed to undo task: %w", err) 1442 + } 1443 + 1444 + fmt.Printf("Undid last change to task (ID: %d)\n", task.ID) 1445 + return nil 1446 + } 1447 + 1448 + // ShowHistory displays the change history for a task 1449 + func (h *TaskHandler) ShowHistory(ctx context.Context, taskID string, limit int) error { 1450 + var task *models.Task 1451 + var err error 1452 + 1453 + if id, err_ := strconv.ParseInt(taskID, 10, 64); err_ == nil { 1454 + task, err = h.repos.Tasks.Get(ctx, id) 1455 + } else { 1456 + task, err = h.repos.Tasks.GetByUUID(ctx, taskID) 1457 + } 1458 + 1459 + if err != nil { 1460 + return fmt.Errorf("failed to find task: %w", err) 1461 + } 1462 + 1463 + history, err := h.repos.Tasks.GetHistory(ctx, task.ID, limit) 1464 + if err != nil { 1465 + return fmt.Errorf("failed to get history: %w", err) 1466 + } 1467 + 1468 + if len(history) == 0 { 1469 + fmt.Printf("No history found for task (ID: %d): %s\n", task.ID, task.Description) 1470 + return nil 1471 + } 1472 + 1473 + fmt.Printf("Task (ID: %d): %s\n", task.ID, task.Description) 1474 + fmt.Printf("History (%d changes):\n\n", len(history)) 1475 + 1476 + for i, h := range history { 1477 + fmt.Printf("%d. [%s] %s at %s\n", i+1, h.Operation, task.Description, h.CreatedAt.Format("2006-01-02 15:04:05")) 1478 + } 1479 + 1480 + return nil 1481 + }
+350 -12
internal/handlers/tasks_test.go
··· 77 77 78 78 t.Run("creates task successfully", func(t *testing.T) { 79 79 desc := "Buy groceries and cook dinner" 80 - err := handler.Create(ctx, desc, "", "", "", "", "", "", "", "", []string{}) 80 + err := handler.Create(ctx, desc, "", "", "", "", "", "", "", "", "", "", []string{}) 81 81 shared.AssertNoError(t, err, "CreateTask should succeed") 82 82 83 83 tasks, err := handler.repos.Tasks.GetPending(ctx) ··· 104 104 105 105 t.Run("fails with empty description", func(t *testing.T) { 106 106 desc := "" 107 - err := handler.Create(ctx, desc, "", "", "", "", "", "", "", "", []string{}) 107 + err := handler.Create(ctx, desc, "", "", "", "", "", "", "", "", "", "", []string{}) 108 108 shared.AssertError(t, err, "Expected error for empty description") 109 109 shared.AssertContains(t, err.Error(), "task description required", "Error message should mention required description") 110 110 }) ··· 116 116 due := "2024-12-31" 117 117 tags := []string{"urgent", "work"} 118 118 119 - err := handler.Create(ctx, description, priority, project, "test-context", due, "", "", "", "", tags) 119 + err := handler.Create(ctx, description, priority, project, "test-context", due, "", "", "", "", "", "", tags) 120 120 if err != nil { 121 121 t.Errorf("CreateTask with flags failed: %v", err) 122 122 } ··· 171 171 desc := "Task with invalid date" 172 172 invalidDue := "invalid-date" 173 173 174 - err := handler.Create(ctx, desc, "", "", "", invalidDue, "", "", "", "", []string{}) 174 + err := handler.Create(ctx, desc, "", "", "", invalidDue, "", "", "", "", "", "", []string{}) 175 175 if err == nil { 176 176 t.Error("Expected error for invalid due date format") 177 177 } ··· 185 185 ctx, cancel := context.WithCancel(ctx) 186 186 cancel() 187 187 188 - err := handler.Create(ctx, "Test task", "", "", "", "", "", "", "", "", []string{}) 188 + err := handler.Create(ctx, "Test task", "", "", "", "", "", "", "", "", "", "", []string{}) 189 189 if err == nil { 190 190 t.Error("Expected error when repository Create fails") 191 191 } ··· 229 229 } 230 230 231 231 t.Run("lists pending tasks by default (static mode)", func(t *testing.T) { 232 - err := handler.List(ctx, true, false, "", "", "", "") 232 + err := handler.List(ctx, true, false, "", "", "", "", "") 233 233 if err != nil { 234 234 t.Errorf("ListTasks failed: %v", err) 235 235 } 236 236 }) 237 237 238 238 t.Run("filters by status (static mode)", func(t *testing.T) { 239 - err := handler.List(ctx, true, false, "completed", "", "", "") 239 + err := handler.List(ctx, true, false, "completed", "", "", "", "") 240 240 if err != nil { 241 241 t.Errorf("ListTasks with status filter failed: %v", err) 242 242 } 243 243 }) 244 244 245 245 t.Run("filters by priority (static mode)", func(t *testing.T) { 246 - err := handler.List(ctx, true, false, "", "A", "", "") 246 + err := handler.List(ctx, true, false, "", "A", "", "", "") 247 247 if err != nil { 248 248 t.Errorf("ListTasks with priority filter failed: %v", err) 249 249 } 250 250 }) 251 251 252 252 t.Run("filters by project (static mode)", func(t *testing.T) { 253 - err := handler.List(ctx, true, false, "", "", "work", "") 253 + err := handler.List(ctx, true, false, "", "", "work", "", "") 254 254 if err != nil { 255 255 t.Errorf("ListTasks with project filter failed: %v", err) 256 256 } 257 257 }) 258 258 259 259 t.Run("show all tasks (static mode)", func(t *testing.T) { 260 - err := handler.List(ctx, true, true, "", "", "", "") 260 + err := handler.List(ctx, true, true, "", "", "", "", "") 261 261 if err != nil { 262 262 t.Errorf("ListTasks with show all failed: %v", err) 263 263 } ··· 1479 1479 } 1480 1480 defer handler.Close() 1481 1481 1482 - err = handler.Create(ctx, "Test Task 1", "high", "test-project", "test-context", "", "", "", "", "", []string{"tag1"}) 1482 + err = handler.Create(ctx, "Test Task 1", "high", "test-project", "test-context", "", "", "", "", "", "", "", []string{"tag1"}) 1483 1483 if err != nil { 1484 1484 t.Fatalf("Failed to create test task: %v", err) 1485 1485 } 1486 1486 1487 - err = handler.Create(ctx, "Test Task 2", "medium", "test-project", "test-context", "", "", "", "", "", []string{"tag2"}) 1487 + err = handler.Create(ctx, "Test Task 2", "medium", "test-project", "test-context", "", "", "", "", "", "", "", []string{"tag2"}) 1488 1488 if err != nil { 1489 1489 t.Fatalf("Failed to create test task: %v", err) 1490 1490 } ··· 2122 2122 if err = handler.BlockedByDep(ctx, strconv.FormatInt(id2, 10)); err != nil { 2123 2123 t.Errorf("BlockedByDep failed: %v", err) 2124 2124 } 2125 + }) 2126 + 2127 + t.Run("Annotate", func(t *testing.T) { 2128 + suite := NewHandlerTestSuite(t) 2129 + defer suite.cleanup() 2130 + 2131 + handler, err := NewTaskHandler() 2132 + if err != nil { 2133 + t.Fatalf("Failed to create handler: %v", err) 2134 + } 2135 + defer handler.Close() 2136 + 2137 + id, err := handler.repos.Tasks.Create(ctx, &models.Task{ 2138 + UUID: uuid.New().String(), 2139 + Description: "Test task", 2140 + Status: "pending", 2141 + }) 2142 + if err != nil { 2143 + t.Fatalf("Failed to create task: %v", err) 2144 + } 2145 + 2146 + t.Run("adds annotation successfully", func(t *testing.T) { 2147 + err := handler.Annotate(ctx, strconv.FormatInt(id, 10), "First annotation") 2148 + shared.AssertNoError(t, err, "Annotate should succeed") 2149 + 2150 + task, err := handler.repos.Tasks.Get(ctx, id) 2151 + shared.AssertNoError(t, err, "Get should succeed") 2152 + shared.AssertEqual(t, 1, len(task.Annotations), "should have 1 annotation") 2153 + shared.AssertEqual(t, "First annotation", task.Annotations[0], "annotation text should match") 2154 + }) 2155 + 2156 + t.Run("adds multiple annotations", func(t *testing.T) { 2157 + err := handler.Annotate(ctx, strconv.FormatInt(id, 10), "Second annotation") 2158 + shared.AssertNoError(t, err, "Annotate should succeed") 2159 + 2160 + task, err := handler.repos.Tasks.Get(ctx, id) 2161 + shared.AssertNoError(t, err, "Get should succeed") 2162 + shared.AssertEqual(t, 2, len(task.Annotations), "should have 2 annotations") 2163 + }) 2164 + 2165 + t.Run("fails with empty annotation", func(t *testing.T) { 2166 + err := handler.Annotate(ctx, strconv.FormatInt(id, 10), "") 2167 + shared.AssertError(t, err, "should fail with empty annotation") 2168 + shared.AssertContains(t, err.Error(), "annotation text required", "error message") 2169 + }) 2170 + 2171 + t.Run("fails with invalid task ID", func(t *testing.T) { 2172 + err := handler.Annotate(ctx, "99999", "Test annotation") 2173 + shared.AssertError(t, err, "should fail with invalid task ID") 2174 + shared.AssertContains(t, err.Error(), "failed to find task", "error message") 2175 + }) 2176 + 2177 + t.Run("works with UUID", func(t *testing.T) { 2178 + task := &models.Task{ 2179 + UUID: uuid.New().String(), 2180 + Description: "UUID task", 2181 + Status: "pending", 2182 + } 2183 + _, err := handler.repos.Tasks.Create(ctx, task) 2184 + shared.AssertNoError(t, err, "Create should succeed") 2185 + 2186 + err = handler.Annotate(ctx, task.UUID, "UUID annotation") 2187 + shared.AssertNoError(t, err, "Annotate with UUID should succeed") 2188 + 2189 + retrieved, err := handler.repos.Tasks.GetByUUID(ctx, task.UUID) 2190 + shared.AssertNoError(t, err, "GetByUUID should succeed") 2191 + shared.AssertEqual(t, 1, len(retrieved.Annotations), "should have 1 annotation") 2192 + }) 2193 + }) 2194 + 2195 + t.Run("ListAnnotations", func(t *testing.T) { 2196 + suite := NewHandlerTestSuite(t) 2197 + defer suite.cleanup() 2198 + 2199 + handler, err := NewTaskHandler() 2200 + if err != nil { 2201 + t.Fatalf("Failed to create handler: %v", err) 2202 + } 2203 + defer handler.Close() 2204 + 2205 + t.Run("lists annotations successfully", func(t *testing.T) { 2206 + task := &models.Task{ 2207 + UUID: uuid.New().String(), 2208 + Description: "Test task", 2209 + Status: "pending", 2210 + Annotations: []string{"Annotation 1", "Annotation 2", "Annotation 3"}, 2211 + } 2212 + id, err := handler.repos.Tasks.Create(ctx, task) 2213 + shared.AssertNoError(t, err, "Create should succeed") 2214 + 2215 + err = handler.ListAnnotations(ctx, strconv.FormatInt(id, 10)) 2216 + shared.AssertNoError(t, err, "ListAnnotations should succeed") 2217 + }) 2218 + 2219 + t.Run("handles task with no annotations", func(t *testing.T) { 2220 + task := &models.Task{ 2221 + UUID: uuid.New().String(), 2222 + Description: "Task without annotations", 2223 + Status: "pending", 2224 + } 2225 + id, err := handler.repos.Tasks.Create(ctx, task) 2226 + shared.AssertNoError(t, err, "Create should succeed") 2227 + 2228 + err = handler.ListAnnotations(ctx, strconv.FormatInt(id, 10)) 2229 + shared.AssertNoError(t, err, "ListAnnotations should succeed for empty annotations") 2230 + }) 2231 + 2232 + t.Run("fails with invalid task ID", func(t *testing.T) { 2233 + err := handler.ListAnnotations(ctx, "99999") 2234 + shared.AssertError(t, err, "should fail with invalid task ID") 2235 + shared.AssertContains(t, err.Error(), "failed to find task", "error message") 2236 + }) 2237 + }) 2238 + 2239 + t.Run("RemoveAnnotation", func(t *testing.T) { 2240 + suite := NewHandlerTestSuite(t) 2241 + defer suite.cleanup() 2242 + 2243 + handler, err := NewTaskHandler() 2244 + if err != nil { 2245 + t.Fatalf("Failed to create handler: %v", err) 2246 + } 2247 + defer handler.Close() 2248 + 2249 + t.Run("removes annotation successfully", func(t *testing.T) { 2250 + task := &models.Task{ 2251 + UUID: uuid.New().String(), 2252 + Description: "Test task", 2253 + Status: "pending", 2254 + Annotations: []string{"First", "Second", "Third"}, 2255 + } 2256 + id, err := handler.repos.Tasks.Create(ctx, task) 2257 + shared.AssertNoError(t, err, "Create should succeed") 2258 + 2259 + err = handler.RemoveAnnotation(ctx, strconv.FormatInt(id, 10), 2) 2260 + shared.AssertNoError(t, err, "RemoveAnnotation should succeed") 2261 + 2262 + retrieved, err := handler.repos.Tasks.Get(ctx, id) 2263 + shared.AssertNoError(t, err, "Get should succeed") 2264 + shared.AssertEqual(t, 2, len(retrieved.Annotations), "should have 2 annotations") 2265 + shared.AssertEqual(t, "First", retrieved.Annotations[0], "first annotation should remain") 2266 + shared.AssertEqual(t, "Third", retrieved.Annotations[1], "third annotation should be second") 2267 + }) 2268 + 2269 + t.Run("fails with invalid index (too low)", func(t *testing.T) { 2270 + task := &models.Task{ 2271 + UUID: uuid.New().String(), 2272 + Description: "Test task", 2273 + Status: "pending", 2274 + Annotations: []string{"First"}, 2275 + } 2276 + id, err := handler.repos.Tasks.Create(ctx, task) 2277 + shared.AssertNoError(t, err, "Create should succeed") 2278 + 2279 + err = handler.RemoveAnnotation(ctx, strconv.FormatInt(id, 10), 0) 2280 + shared.AssertError(t, err, "should fail with index 0") 2281 + shared.AssertContains(t, err.Error(), "index out of range", "error message") 2282 + }) 2283 + 2284 + t.Run("fails with invalid index (too high)", func(t *testing.T) { 2285 + task := &models.Task{ 2286 + UUID: uuid.New().String(), 2287 + Description: "Test task", 2288 + Status: "pending", 2289 + Annotations: []string{"First"}, 2290 + } 2291 + id, err := handler.repos.Tasks.Create(ctx, task) 2292 + shared.AssertNoError(t, err, "Create should succeed") 2293 + 2294 + err = handler.RemoveAnnotation(ctx, strconv.FormatInt(id, 10), 5) 2295 + shared.AssertError(t, err, "should fail with index > len") 2296 + shared.AssertContains(t, err.Error(), "index out of range", "error message") 2297 + }) 2298 + 2299 + t.Run("fails when task has no annotations", func(t *testing.T) { 2300 + task := &models.Task{ 2301 + UUID: uuid.New().String(), 2302 + Description: "Task without annotations", 2303 + Status: "pending", 2304 + } 2305 + id, err := handler.repos.Tasks.Create(ctx, task) 2306 + shared.AssertNoError(t, err, "Create should succeed") 2307 + 2308 + err = handler.RemoveAnnotation(ctx, strconv.FormatInt(id, 10), 1) 2309 + shared.AssertError(t, err, "should fail when no annotations") 2310 + shared.AssertContains(t, err.Error(), "has no annotations", "error message") 2311 + }) 2312 + 2313 + t.Run("fails with invalid task ID", func(t *testing.T) { 2314 + err := handler.RemoveAnnotation(ctx, "99999", 1) 2315 + shared.AssertError(t, err, "should fail with invalid task ID") 2316 + shared.AssertContains(t, err.Error(), "failed to find task", "error message") 2317 + }) 2318 + }) 2319 + 2320 + t.Run("BulkEdit", func(t *testing.T) { 2321 + suite := NewHandlerTestSuite(t) 2322 + defer suite.cleanup() 2323 + 2324 + handler, err := NewTaskHandler() 2325 + if err != nil { 2326 + t.Fatalf("Failed to create handler: %v", err) 2327 + } 2328 + defer handler.Close() 2329 + 2330 + t.Run("updates multiple tasks successfully", func(t *testing.T) { 2331 + id1, err := handler.repos.Tasks.Create(ctx, &models.Task{ 2332 + UUID: uuid.New().String(), 2333 + Description: "Task 1", 2334 + Status: "pending", 2335 + }) 2336 + shared.AssertNoError(t, err, "Create should succeed") 2337 + 2338 + id2, err := handler.repos.Tasks.Create(ctx, &models.Task{ 2339 + UUID: uuid.New().String(), 2340 + Description: "Task 2", 2341 + Status: "pending", 2342 + }) 2343 + shared.AssertNoError(t, err, "Create should succeed") 2344 + 2345 + taskIDs := []string{strconv.FormatInt(id1, 10), strconv.FormatInt(id2, 10)} 2346 + err = handler.BulkEdit(ctx, taskIDs, "done", "high", "test-project", "", []string{}, false, false) 2347 + shared.AssertNoError(t, err, "BulkEdit should succeed") 2348 + 2349 + task1, err := handler.repos.Tasks.Get(ctx, id1) 2350 + shared.AssertNoError(t, err, "Get should succeed") 2351 + shared.AssertEqual(t, "done", task1.Status, "task 1 status should be updated") 2352 + shared.AssertEqual(t, "high", task1.Priority, "task 1 priority should be updated") 2353 + shared.AssertEqual(t, "test-project", task1.Project, "task 1 project should be updated") 2354 + 2355 + task2, err := handler.repos.Tasks.Get(ctx, id2) 2356 + shared.AssertNoError(t, err, "Get should succeed") 2357 + shared.AssertEqual(t, "done", task2.Status, "task 2 status should be updated") 2358 + shared.AssertEqual(t, "high", task2.Priority, "task 2 priority should be updated") 2359 + shared.AssertEqual(t, "test-project", task2.Project, "task 2 project should be updated") 2360 + }) 2361 + 2362 + t.Run("updates with tag replacement", func(t *testing.T) { 2363 + id1, err := handler.repos.Tasks.Create(ctx, &models.Task{ 2364 + UUID: uuid.New().String(), 2365 + Description: "Task 1", 2366 + Status: "pending", 2367 + Tags: []string{"old-tag"}, 2368 + }) 2369 + shared.AssertNoError(t, err, "Create should succeed") 2370 + 2371 + taskIDs := []string{strconv.FormatInt(id1, 10)} 2372 + err = handler.BulkEdit(ctx, taskIDs, "", "", "", "", []string{"new-tag1", "new-tag2"}, false, false) 2373 + shared.AssertNoError(t, err, "BulkEdit should succeed") 2374 + 2375 + task, err := handler.repos.Tasks.Get(ctx, id1) 2376 + shared.AssertNoError(t, err, "Get should succeed") 2377 + shared.AssertEqual(t, 2, len(task.Tags), "should have 2 tags") 2378 + shared.AssertTrue(t, slices.Contains(task.Tags, "new-tag1"), "should contain new-tag1") 2379 + shared.AssertTrue(t, slices.Contains(task.Tags, "new-tag2"), "should contain new-tag2") 2380 + }) 2381 + 2382 + t.Run("adds tags with add-tags flag", func(t *testing.T) { 2383 + id1, err := handler.repos.Tasks.Create(ctx, &models.Task{ 2384 + UUID: uuid.New().String(), 2385 + Description: "Task 1", 2386 + Status: "pending", 2387 + Tags: []string{"existing-tag"}, 2388 + }) 2389 + shared.AssertNoError(t, err, "Create should succeed") 2390 + 2391 + taskIDs := []string{strconv.FormatInt(id1, 10)} 2392 + err = handler.BulkEdit(ctx, taskIDs, "", "", "", "", []string{"new-tag"}, true, false) 2393 + shared.AssertNoError(t, err, "BulkEdit should succeed") 2394 + 2395 + task, err := handler.repos.Tasks.Get(ctx, id1) 2396 + shared.AssertNoError(t, err, "Get should succeed") 2397 + shared.AssertEqual(t, 2, len(task.Tags), "should have 2 tags") 2398 + shared.AssertTrue(t, slices.Contains(task.Tags, "existing-tag"), "should contain existing-tag") 2399 + shared.AssertTrue(t, slices.Contains(task.Tags, "new-tag"), "should contain new-tag") 2400 + }) 2401 + 2402 + t.Run("removes tags with remove-tags flag", func(t *testing.T) { 2403 + id1, err := handler.repos.Tasks.Create(ctx, &models.Task{ 2404 + UUID: uuid.New().String(), 2405 + Description: "Task 1", 2406 + Status: "pending", 2407 + Tags: []string{"tag1", "tag2", "tag3"}, 2408 + }) 2409 + shared.AssertNoError(t, err, "Create should succeed") 2410 + 2411 + taskIDs := []string{strconv.FormatInt(id1, 10)} 2412 + err = handler.BulkEdit(ctx, taskIDs, "", "", "", "", []string{"tag2"}, false, true) 2413 + shared.AssertNoError(t, err, "BulkEdit should succeed") 2414 + 2415 + task, err := handler.repos.Tasks.Get(ctx, id1) 2416 + shared.AssertNoError(t, err, "Get should succeed") 2417 + shared.AssertEqual(t, 2, len(task.Tags), "should have 2 tags") 2418 + shared.AssertTrue(t, slices.Contains(task.Tags, "tag1"), "should contain tag1") 2419 + shared.AssertTrue(t, slices.Contains(task.Tags, "tag3"), "should contain tag3") 2420 + shared.AssertFalse(t, slices.Contains(task.Tags, "tag2"), "should not contain tag2") 2421 + }) 2422 + 2423 + t.Run("fails with no task IDs", func(t *testing.T) { 2424 + err := handler.BulkEdit(ctx, []string{}, "done", "", "", "", []string{}, false, false) 2425 + shared.AssertError(t, err, "should fail with no task IDs") 2426 + shared.AssertContains(t, err.Error(), "no task IDs provided", "error message") 2427 + }) 2428 + 2429 + t.Run("fails with invalid task ID", func(t *testing.T) { 2430 + err := handler.BulkEdit(ctx, []string{"99999"}, "done", "", "", "", []string{}, false, false) 2431 + shared.AssertError(t, err, "should fail with invalid task ID") 2432 + }) 2433 + 2434 + t.Run("works with UUIDs", func(t *testing.T) { 2435 + task1 := &models.Task{ 2436 + UUID: uuid.New().String(), 2437 + Description: "UUID task 1", 2438 + Status: "pending", 2439 + } 2440 + _, err := handler.repos.Tasks.Create(ctx, task1) 2441 + shared.AssertNoError(t, err, "Create should succeed") 2442 + 2443 + task2 := &models.Task{ 2444 + UUID: uuid.New().String(), 2445 + Description: "UUID task 2", 2446 + Status: "pending", 2447 + } 2448 + _, err = handler.repos.Tasks.Create(ctx, task2) 2449 + shared.AssertNoError(t, err, "Create should succeed") 2450 + 2451 + taskIDs := []string{task1.UUID, task2.UUID} 2452 + err = handler.BulkEdit(ctx, taskIDs, "done", "", "", "", []string{}, false, false) 2453 + shared.AssertNoError(t, err, "BulkEdit with UUIDs should succeed") 2454 + 2455 + retrieved1, err := handler.repos.Tasks.GetByUUID(ctx, task1.UUID) 2456 + shared.AssertNoError(t, err, "GetByUUID should succeed") 2457 + shared.AssertEqual(t, "done", retrieved1.Status, "task 1 status should be updated") 2458 + 2459 + retrieved2, err := handler.repos.Tasks.GetByUUID(ctx, task2.UUID) 2460 + shared.AssertNoError(t, err, "GetByUUID should succeed") 2461 + shared.AssertEqual(t, "done", retrieved2.Status, "task 2 status should be updated") 2462 + }) 2125 2463 }) 2126 2464 }
+107 -5
internal/models/models.go
··· 112 112 Context string `json:"context,omitempty"` 113 113 Tags []string `json:"tags,omitempty"` 114 114 Due *time.Time `json:"due,omitempty"` 115 + Wait *time.Time `json:"wait,omitempty"` // Task is not actionable until this date 116 + Scheduled *time.Time `json:"scheduled,omitempty"` // Task is scheduled to start on this date 115 117 Entry time.Time `json:"entry"` 116 118 Modified time.Time `json:"modified"` 117 119 End *time.Time `json:"end,omitempty"` // Completion time ··· 217 219 HTMLPath string `json:"html_path"` 218 220 Created time.Time `json:"created"` 219 221 Modified time.Time `json:"modified"` 222 + } 223 + 224 + // TaskHistory represents a historical snapshot of a task for undo functionality 225 + type TaskHistory struct { 226 + ID int64 `json:"id"` 227 + TaskID int64 `json:"task_id"` 228 + Operation string `json:"operation"` // update, delete 229 + Snapshot string `json:"snapshot"` // JSON snapshot of task 230 + CreatedAt time.Time `json:"created_at"` 220 231 } 221 232 222 233 // MarshalTags converts tags slice to JSON string for database storage ··· 342 353 // HasDueDate returns true if the task has a due date set. 343 354 func (t *Task) HasDueDate() bool { return t.Due != nil } 344 355 356 + // IsWaiting returns true if the task has a wait date and it hasn't passed yet. 357 + func (t *Task) IsWaiting(now time.Time) bool { 358 + return t.Wait != nil && now.Before(*t.Wait) 359 + } 360 + 361 + // HasWaitDate returns true if the task has a wait date set. 362 + func (t *Task) HasWaitDate() bool { return t.Wait != nil } 363 + 364 + // IsScheduled returns true if the task has a scheduled date. 365 + func (t *Task) IsScheduled() bool { return t.Scheduled != nil } 366 + 367 + // IsActionable returns true if the task can be worked on now. 368 + // A task is actionable if it's not waiting, not blocked, and not completed. 369 + func (t *Task) IsActionable(now time.Time) bool { 370 + if t.IsCompleted() || t.IsDone() || t.IsAbandoned() || t.IsBlocked() { 371 + return false 372 + } 373 + if t.IsWaiting(now) { 374 + return false 375 + } 376 + return true 377 + } 378 + 345 379 // IsRecurring returns true if the task has recurrence defined. 346 380 func (t *Task) IsRecurring() bool { return t.Recur != "" } 347 381 ··· 358 392 return slices.Contains(other.DependsOn, t.UUID) 359 393 } 360 394 361 - // Urgency computes a score based on priority, due date, and tags. 362 - // This can be expanded later with weights. 395 + // Urgency computes a comprehensive score based on multiple factors. 396 + // Higher score means more urgent. Score components: 397 + // - Priority: 0-10 based on priority weight 398 + // - Due date: 0-12 based on proximity (overdue gets highest) 399 + // - Scheduled: 0-4 if scheduled soon 400 + // - Age: 0-2 for old tasks 401 + // - Tags: 0.5 per tag (capped at 2.0) 402 + // - Waiting: -5.0 if not yet actionable 403 + // - Blocked: -3.0 if has incomplete dependencies 363 404 func (t *Task) Urgency(now time.Time) float64 { 405 + if !t.IsActionable(now) { 406 + if t.IsWaiting(now) { 407 + return -5.0 408 + } 409 + if t.IsBlocked() { 410 + return -3.0 411 + } 412 + return -10.0 413 + } 414 + 364 415 score := 0.0 365 - if t.Priority != "" { 366 - score += 1.0 416 + 417 + if t.HasPriority() { 418 + weight := t.GetPriorityWeight() 419 + if weight >= 20 { 420 + score += float64(weight-15) / 2.0 421 + } else if weight > 0 { 422 + score += float64(weight) * 2.0 423 + } 367 424 } 368 - if t.IsOverdue(now) { 425 + 426 + if t.HasDueDate() { 427 + daysUntilDue := t.Due.Sub(now).Hours() / 24.0 428 + if daysUntilDue < 0 { 429 + overdueDays := -daysUntilDue 430 + score += 12.0 + min(overdueDays*0.5, 3.0) 431 + } else if daysUntilDue <= 1 { 432 + score += 10.0 433 + } else if daysUntilDue <= 3 { 434 + score += 8.0 435 + } else if daysUntilDue <= 7 { 436 + score += 6.0 437 + } else if daysUntilDue <= 14 { 438 + score += 4.0 439 + } else if daysUntilDue <= 30 { 440 + score += 2.0 441 + } 442 + } 443 + 444 + if t.IsScheduled() { 445 + daysUntilScheduled := t.Scheduled.Sub(now).Hours() / 24.0 446 + if daysUntilScheduled <= 0 { 447 + score += 4.0 448 + } else if daysUntilScheduled <= 1 { 449 + score += 3.0 450 + } else if daysUntilScheduled <= 3 { 451 + score += 2.0 452 + } else if daysUntilScheduled <= 7 { 453 + score += 1.0 454 + } 455 + } 456 + 457 + age := now.Sub(t.Entry).Hours() / 24.0 458 + if age > 90 { 369 459 score += 2.0 460 + } else if age > 30 { 461 + score += 1.5 462 + } else if age > 14 { 463 + score += 1.0 464 + } else if age > 7 { 465 + score += 0.5 370 466 } 467 + 371 468 if len(t.Tags) > 0 { 469 + score += min(float64(len(t.Tags))*0.5, 2.0) 470 + } 471 + 472 + if t.Project != "" { 372 473 score += 0.5 373 474 } 475 + 374 476 return score 375 477 } 376 478
+238
internal/repo/document_repository.go
··· 1 + package repo 2 + 3 + import ( 4 + "context" 5 + "database/sql" 6 + "fmt" 7 + 8 + "github.com/stormlightlabs/noteleaf/internal/documents" 9 + ) 10 + 11 + func DocumentNotFoundError(id int64) error { 12 + return fmt.Errorf("document with id %d not found", id) 13 + } 14 + 15 + // DocumentRepository provides database operations for documents 16 + type DocumentRepository struct { 17 + db *sql.DB 18 + } 19 + 20 + // NewDocumentRepository creates a new document repository 21 + func NewDocumentRepository(db *sql.DB) *DocumentRepository { 22 + return &DocumentRepository{db: db} 23 + } 24 + 25 + // scanDocument scans a database row into a Document model 26 + func (r *DocumentRepository) scanDocument(s scanner) (*documents.Document, error) { 27 + var doc documents.Document 28 + err := s.Scan(&doc.ID, &doc.Title, &doc.Body, &doc.CreatedAt, &doc.DocKind) 29 + if err != nil { 30 + return nil, err 31 + } 32 + return &doc, nil 33 + } 34 + 35 + // queryOne executes a query that returns a single document 36 + func (r *DocumentRepository) queryOne(ctx context.Context, query string, args ...any) (*documents.Document, error) { 37 + row := r.db.QueryRowContext(ctx, query, args...) 38 + doc, err := r.scanDocument(row) 39 + if err != nil { 40 + if err == sql.ErrNoRows { 41 + return nil, fmt.Errorf("document not found") 42 + } 43 + return nil, fmt.Errorf("failed to scan document: %w", err) 44 + } 45 + return doc, nil 46 + } 47 + 48 + // queryMany executes a query that returns multiple documents 49 + func (r *DocumentRepository) queryMany(ctx context.Context, query string, args ...any) ([]documents.Document, error) { 50 + rows, err := r.db.QueryContext(ctx, query, args...) 51 + if err != nil { 52 + return nil, fmt.Errorf("failed to query documents: %w", err) 53 + } 54 + defer rows.Close() 55 + 56 + var docs []documents.Document 57 + for rows.Next() { 58 + doc, err := r.scanDocument(rows) 59 + if err != nil { 60 + return nil, fmt.Errorf("failed to scan document: %w", err) 61 + } 62 + docs = append(docs, *doc) 63 + } 64 + 65 + if err := rows.Err(); err != nil { 66 + return nil, fmt.Errorf("error iterating over documents: %w", err) 67 + } 68 + 69 + return docs, nil 70 + } 71 + 72 + // Create stores a new document and returns its assigned ID 73 + func (r *DocumentRepository) Create(ctx context.Context, doc *documents.Document) (int64, error) { 74 + result, err := r.db.ExecContext(ctx, queryDocumentInsert, 75 + doc.Title, doc.Body, doc.CreatedAt, doc.DocKind) 76 + if err != nil { 77 + return 0, fmt.Errorf("failed to insert document: %w", err) 78 + } 79 + 80 + id, err := result.LastInsertId() 81 + if err != nil { 82 + return 0, fmt.Errorf("failed to get last insert id: %w", err) 83 + } 84 + 85 + doc.ID = id 86 + return id, nil 87 + } 88 + 89 + // Get retrieves a document by its ID 90 + func (r *DocumentRepository) Get(ctx context.Context, id int64) (*documents.Document, error) { 91 + doc, err := r.queryOne(ctx, queryDocumentByID, id) 92 + if err != nil { 93 + return nil, DocumentNotFoundError(id) 94 + } 95 + return doc, nil 96 + } 97 + 98 + // Delete removes a document by its ID 99 + func (r *DocumentRepository) Delete(ctx context.Context, id int64) error { 100 + result, err := r.db.ExecContext(ctx, queryDocumentDelete, id) 101 + if err != nil { 102 + return fmt.Errorf("failed to delete document: %w", err) 103 + } 104 + 105 + rowsAffected, err := result.RowsAffected() 106 + if err != nil { 107 + return fmt.Errorf("failed to get rows affected: %w", err) 108 + } 109 + 110 + if rowsAffected == 0 { 111 + return DocumentNotFoundError(id) 112 + } 113 + 114 + return nil 115 + } 116 + 117 + // List retrieves all documents 118 + func (r *DocumentRepository) List(ctx context.Context) ([]documents.Document, error) { 119 + return r.queryMany(ctx, queryDocumentsList) 120 + } 121 + 122 + // ListByKind retrieves documents of a specific kind 123 + func (r *DocumentRepository) ListByKind(ctx context.Context, kind documents.DocKind) ([]documents.Document, error) { 124 + return r.queryMany(ctx, queryDocumentsByKind, int64(kind)) 125 + } 126 + 127 + // DeleteAll removes all documents from the database 128 + func (r *DocumentRepository) DeleteAll(ctx context.Context) error { 129 + _, err := r.db.ExecContext(ctx, queryDocumentsDeleteAll) 130 + if err != nil { 131 + return fmt.Errorf("failed to delete all documents: %w", err) 132 + } 133 + return nil 134 + } 135 + 136 + // RebuildFromNotes rebuilds the documents table from notes 137 + func (r *DocumentRepository) RebuildFromNotes(ctx context.Context, noteRepo *NoteRepository) error { 138 + notes, err := noteRepo.List(ctx, NoteListOptions{}) 139 + if err != nil { 140 + return fmt.Errorf("failed to list notes: %w", err) 141 + } 142 + 143 + for _, note := range notes { 144 + doc := &documents.Document{ 145 + Title: note.Title, 146 + Body: note.Content, 147 + CreatedAt: note.Created, 148 + DocKind: int64(documents.NoteDoc), 149 + } 150 + 151 + if _, err := r.Create(ctx, doc); err != nil { 152 + return fmt.Errorf("failed to create document from note %d: %w", note.ID, err) 153 + } 154 + } 155 + 156 + return nil 157 + } 158 + 159 + // BuildIndex creates a TF-IDF index from all documents in the database 160 + func (r *DocumentRepository) BuildIndex(ctx context.Context) (*documents.Index, error) { 161 + docs, err := r.List(ctx) 162 + if err != nil { 163 + return nil, fmt.Errorf("failed to list documents: %w", err) 164 + } 165 + 166 + return documents.BuildIndex(docs), nil 167 + } 168 + 169 + // SearchEngine wraps a DocumentRepository with search capabilities 170 + type SearchEngine struct { 171 + repo *DocumentRepository 172 + index *documents.Index 173 + } 174 + 175 + // NewSearchEngine creates a new search engine with the given repository 176 + func NewSearchEngine(repo *DocumentRepository) *SearchEngine { 177 + return &SearchEngine{ 178 + repo: repo, 179 + index: nil, 180 + } 181 + } 182 + 183 + // Rebuild rebuilds the search index from the database 184 + func (se *SearchEngine) Rebuild(ctx context.Context) error { 185 + idx, err := se.repo.BuildIndex(ctx) 186 + if err != nil { 187 + return fmt.Errorf("failed to build index: %w", err) 188 + } 189 + 190 + se.index = idx 191 + return nil 192 + } 193 + 194 + // Search performs a TF-IDF search and returns matching documents 195 + func (se *SearchEngine) Search(ctx context.Context, query string, limit int) ([]documents.Document, error) { 196 + if se.index == nil { 197 + return nil, fmt.Errorf("search index not initialized") 198 + } 199 + 200 + results, err := se.index.Search(query, limit) 201 + if err != nil { 202 + return nil, fmt.Errorf("failed to search: %w", err) 203 + } 204 + 205 + docs := make([]documents.Document, 0, len(results)) 206 + for _, result := range results { 207 + doc, err := se.repo.Get(ctx, result.DocID) 208 + if err != nil { 209 + return nil, fmt.Errorf("failed to get document %d: %w", result.DocID, err) 210 + } 211 + docs = append(docs, *doc) 212 + } 213 + 214 + return docs, nil 215 + } 216 + 217 + // SearchWithScores performs a TF-IDF search and returns results with scores 218 + func (se *SearchEngine) SearchWithScores(ctx context.Context, query string, limit int) ([]documents.Result, []documents.Document, error) { 219 + if se.index == nil { 220 + return nil, nil, fmt.Errorf("search index not initialized") 221 + } 222 + 223 + results, err := se.index.Search(query, limit) 224 + if err != nil { 225 + return nil, nil, fmt.Errorf("failed to search: %w", err) 226 + } 227 + 228 + docs := make([]documents.Document, 0, len(results)) 229 + for _, result := range results { 230 + doc, err := se.repo.Get(ctx, result.DocID) 231 + if err != nil { 232 + return nil, nil, fmt.Errorf("failed to get document %d: %w", result.DocID, err) 233 + } 234 + docs = append(docs, *doc) 235 + } 236 + 237 + return results, docs, nil 238 + }
+379
internal/repo/document_repository_test.go
··· 1 + package repo 2 + 3 + import ( 4 + "context" 5 + "testing" 6 + "time" 7 + 8 + "github.com/stormlightlabs/noteleaf/internal/documents" 9 + "github.com/stormlightlabs/noteleaf/internal/shared" 10 + ) 11 + 12 + func CreateSampleDocument() *documents.Document { 13 + return &documents.Document{ 14 + Title: "Test Document", 15 + Body: "This is test content for searching", 16 + CreatedAt: time.Now(), 17 + DocKind: int64(documents.NoteDoc), 18 + } 19 + } 20 + 21 + func TestDocumentRepository(t *testing.T) { 22 + db := CreateTestDB(t) 23 + repo := NewDocumentRepository(db) 24 + ctx := context.Background() 25 + 26 + t.Run("Create", func(t *testing.T) { 27 + t.Run("creates document successfully", func(t *testing.T) { 28 + doc := CreateSampleDocument() 29 + id, err := repo.Create(ctx, doc) 30 + 31 + shared.AssertNoError(t, err, "create should succeed") 32 + shared.AssertTrue(t, id > 0, "id should be positive") 33 + shared.AssertEqual(t, id, doc.ID, "document ID should be set") 34 + }) 35 + 36 + t.Run("returns error with cancelled context", func(t *testing.T) { 37 + doc := CreateSampleDocument() 38 + canceledCtx := NewCanceledContext() 39 + 40 + _, err := repo.Create(canceledCtx, doc) 41 + AssertCancelledContext(t, err) 42 + }) 43 + }) 44 + 45 + t.Run("Get", func(t *testing.T) { 46 + t.Run("retrieves existing document", func(t *testing.T) { 47 + doc := CreateSampleDocument() 48 + id, err := repo.Create(ctx, doc) 49 + shared.AssertNoError(t, err, "create should succeed") 50 + 51 + retrieved, err := repo.Get(ctx, id) 52 + shared.AssertNoError(t, err, "get should succeed") 53 + shared.AssertEqual(t, doc.Title, retrieved.Title, "title should match") 54 + shared.AssertEqual(t, doc.Body, retrieved.Body, "body should match") 55 + shared.AssertEqual(t, doc.DocKind, retrieved.DocKind, "doc kind should match") 56 + }) 57 + 58 + t.Run("returns error for non-existent document", func(t *testing.T) { 59 + _, err := repo.Get(ctx, 99999) 60 + shared.AssertError(t, err, "should return error for non-existent document") 61 + shared.AssertContains(t, err.Error(), "not found", "error should mention not found") 62 + }) 63 + 64 + t.Run("returns error with cancelled context", func(t *testing.T) { 65 + canceledCtx := NewCanceledContext() 66 + _, err := repo.Get(canceledCtx, 1) 67 + AssertCancelledContext(t, err) 68 + }) 69 + }) 70 + 71 + t.Run("Delete", func(t *testing.T) { 72 + t.Run("deletes existing document", func(t *testing.T) { 73 + doc := CreateSampleDocument() 74 + id, err := repo.Create(ctx, doc) 75 + shared.AssertNoError(t, err, "create should succeed") 76 + 77 + err = repo.Delete(ctx, id) 78 + shared.AssertNoError(t, err, "delete should succeed") 79 + 80 + _, err = repo.Get(ctx, id) 81 + shared.AssertError(t, err, "get after delete should fail") 82 + }) 83 + 84 + t.Run("returns error for non-existent document", func(t *testing.T) { 85 + err := repo.Delete(ctx, 99999) 86 + shared.AssertError(t, err, "should return error for non-existent document") 87 + }) 88 + 89 + t.Run("returns error with cancelled context", func(t *testing.T) { 90 + canceledCtx := NewCanceledContext() 91 + err := repo.Delete(canceledCtx, 1) 92 + AssertCancelledContext(t, err) 93 + }) 94 + }) 95 + 96 + t.Run("List", func(t *testing.T) { 97 + t.Run("returns all documents", func(t *testing.T) { 98 + db := CreateTestDB(t) 99 + repo := NewDocumentRepository(db) 100 + 101 + doc1 := CreateSampleDocument() 102 + doc1.Title = "First" 103 + doc2 := CreateSampleDocument() 104 + doc2.Title = "Second" 105 + 106 + _, err := repo.Create(ctx, doc1) 107 + shared.AssertNoError(t, err, "create doc1 should succeed") 108 + _, err = repo.Create(ctx, doc2) 109 + shared.AssertNoError(t, err, "create doc2 should succeed") 110 + 111 + docs, err := repo.List(ctx) 112 + shared.AssertNoError(t, err, "list should succeed") 113 + shared.AssertEqual(t, 2, len(docs), "should return 2 documents") 114 + }) 115 + 116 + t.Run("returns empty list when no documents exist", func(t *testing.T) { 117 + db := CreateTestDB(t) 118 + repo := NewDocumentRepository(db) 119 + 120 + docs, err := repo.List(ctx) 121 + shared.AssertNoError(t, err, "list should succeed") 122 + shared.AssertEqual(t, 0, len(docs), "should return empty list") 123 + }) 124 + 125 + t.Run("returns error with cancelled context", func(t *testing.T) { 126 + canceledCtx := NewCanceledContext() 127 + _, err := repo.List(canceledCtx) 128 + AssertCancelledContext(t, err) 129 + }) 130 + }) 131 + 132 + t.Run("ListByKind", func(t *testing.T) { 133 + t.Run("filters documents by kind", func(t *testing.T) { 134 + db := CreateTestDB(t) 135 + repo := NewDocumentRepository(db) 136 + 137 + noteDoc := CreateSampleDocument() 138 + noteDoc.DocKind = int64(documents.NoteDoc) 139 + articleDoc := CreateSampleDocument() 140 + articleDoc.DocKind = int64(documents.ArticleDoc) 141 + 142 + _, err := repo.Create(ctx, noteDoc) 143 + shared.AssertNoError(t, err, "create note should succeed") 144 + _, err = repo.Create(ctx, articleDoc) 145 + shared.AssertNoError(t, err, "create article should succeed") 146 + 147 + notes, err := repo.ListByKind(ctx, documents.NoteDoc) 148 + shared.AssertNoError(t, err, "list by kind should succeed") 149 + shared.AssertEqual(t, 1, len(notes), "should return 1 note") 150 + shared.AssertEqual(t, int64(documents.NoteDoc), notes[0].DocKind, "should be note kind") 151 + }) 152 + 153 + t.Run("returns error with cancelled context", func(t *testing.T) { 154 + canceledCtx := NewCanceledContext() 155 + _, err := repo.ListByKind(canceledCtx, documents.NoteDoc) 156 + AssertCancelledContext(t, err) 157 + }) 158 + }) 159 + 160 + t.Run("DeleteAll", func(t *testing.T) { 161 + t.Run("removes all documents", func(t *testing.T) { 162 + db := CreateTestDB(t) 163 + repo := NewDocumentRepository(db) 164 + 165 + _, err := repo.Create(ctx, CreateSampleDocument()) 166 + shared.AssertNoError(t, err, "create should succeed") 167 + _, err = repo.Create(ctx, CreateSampleDocument()) 168 + shared.AssertNoError(t, err, "create should succeed") 169 + 170 + err = repo.DeleteAll(ctx) 171 + shared.AssertNoError(t, err, "delete all should succeed") 172 + 173 + docs, err := repo.List(ctx) 174 + shared.AssertNoError(t, err, "list should succeed") 175 + shared.AssertEqual(t, 0, len(docs), "should have no documents after delete all") 176 + }) 177 + 178 + t.Run("returns error with cancelled context", func(t *testing.T) { 179 + canceledCtx := NewCanceledContext() 180 + err := repo.DeleteAll(canceledCtx) 181 + AssertCancelledContext(t, err) 182 + }) 183 + }) 184 + 185 + t.Run("RebuildFromNotes", func(t *testing.T) { 186 + t.Run("creates documents from notes", func(t *testing.T) { 187 + db := CreateTestDB(t) 188 + repo := NewDocumentRepository(db) 189 + noteRepo := NewNoteRepository(db) 190 + 191 + note1 := CreateSampleNote() 192 + note1.Title = "Note 1" 193 + note2 := CreateSampleNote() 194 + note2.Title = "Note 2" 195 + 196 + _, err := noteRepo.Create(ctx, note1) 197 + shared.AssertNoError(t, err, "create note1 should succeed") 198 + _, err = noteRepo.Create(ctx, note2) 199 + shared.AssertNoError(t, err, "create note2 should succeed") 200 + 201 + err = repo.RebuildFromNotes(ctx, noteRepo) 202 + shared.AssertNoError(t, err, "rebuild should succeed") 203 + 204 + docs, err := repo.List(ctx) 205 + shared.AssertNoError(t, err, "list should succeed") 206 + shared.AssertEqual(t, 2, len(docs), "should have 2 documents from notes") 207 + }) 208 + 209 + t.Run("returns error with cancelled context", func(t *testing.T) { 210 + db := CreateTestDB(t) 211 + repo := NewDocumentRepository(db) 212 + noteRepo := NewNoteRepository(db) 213 + canceledCtx := NewCanceledContext() 214 + 215 + err := repo.RebuildFromNotes(canceledCtx, noteRepo) 216 + AssertCancelledContext(t, err) 217 + }) 218 + }) 219 + 220 + t.Run("BuildIndex", func(t *testing.T) { 221 + t.Run("creates search index from documents", func(t *testing.T) { 222 + db := CreateTestDB(t) 223 + repo := NewDocumentRepository(db) 224 + 225 + doc1 := CreateSampleDocument() 226 + doc1.Title = "Go Programming" 227 + doc1.Body = "Learn Go language" 228 + doc2 := CreateSampleDocument() 229 + doc2.Title = "Python Guide" 230 + doc2.Body = "Python tutorial" 231 + 232 + _, err := repo.Create(ctx, doc1) 233 + shared.AssertNoError(t, err, "create doc1 should succeed") 234 + _, err = repo.Create(ctx, doc2) 235 + shared.AssertNoError(t, err, "create doc2 should succeed") 236 + 237 + idx, err := repo.BuildIndex(ctx) 238 + shared.AssertNoError(t, err, "build index should succeed") 239 + shared.AssertNotNil(t, idx, "index should not be nil") 240 + shared.AssertEqual(t, 2, idx.NumDocs, "index should contain 2 documents") 241 + }) 242 + 243 + t.Run("handles empty document set", func(t *testing.T) { 244 + db := CreateTestDB(t) 245 + repo := NewDocumentRepository(db) 246 + 247 + idx, err := repo.BuildIndex(ctx) 248 + shared.AssertNoError(t, err, "build index should succeed with empty set") 249 + shared.AssertEqual(t, 0, idx.NumDocs, "index should be empty") 250 + }) 251 + 252 + t.Run("returns error with cancelled context", func(t *testing.T) { 253 + canceledCtx := NewCanceledContext() 254 + _, err := repo.BuildIndex(canceledCtx) 255 + AssertCancelledContext(t, err) 256 + }) 257 + }) 258 + } 259 + 260 + func TestSearchEngine(t *testing.T) { 261 + db := CreateTestDB(t) 262 + docRepo := NewDocumentRepository(db) 263 + ctx := context.Background() 264 + 265 + doc1 := CreateSampleDocument() 266 + doc1.Title = "Go Programming" 267 + doc1.Body = "Learn Go programming language with examples" 268 + doc2 := CreateSampleDocument() 269 + doc2.Title = "Python Tutorial" 270 + doc2.Body = "Python is a versatile programming language" 271 + doc3 := CreateSampleDocument() 272 + doc3.Title = "Go Advanced" 273 + doc3.Body = "Advanced Go concepts and patterns" 274 + 275 + _, err := docRepo.Create(ctx, doc1) 276 + shared.AssertNoError(t, err, "create doc1 should succeed") 277 + _, err = docRepo.Create(ctx, doc2) 278 + shared.AssertNoError(t, err, "create doc2 should succeed") 279 + _, err = docRepo.Create(ctx, doc3) 280 + shared.AssertNoError(t, err, "create doc3 should succeed") 281 + 282 + t.Run("Rebuild", func(t *testing.T) { 283 + t.Run("builds search index", func(t *testing.T) { 284 + engine := NewSearchEngine(docRepo) 285 + err := engine.Rebuild(ctx) 286 + shared.AssertNoError(t, err, "rebuild should succeed") 287 + shared.AssertNotNil(t, engine.index, "index should be set after rebuild") 288 + }) 289 + 290 + t.Run("returns error with cancelled context", func(t *testing.T) { 291 + engine := NewSearchEngine(docRepo) 292 + canceledCtx := NewCanceledContext() 293 + err := engine.Rebuild(canceledCtx) 294 + AssertCancelledContext(t, err) 295 + }) 296 + }) 297 + 298 + t.Run("Search", func(t *testing.T) { 299 + t.Run("returns error when index not initialized", func(t *testing.T) { 300 + engine := NewSearchEngine(docRepo) 301 + _, err := engine.Search(ctx, "go", 10) 302 + shared.AssertError(t, err, "should error when index not initialized") 303 + shared.AssertContains(t, err.Error(), "not initialized", "error should mention not initialized") 304 + }) 305 + 306 + t.Run("finds matching documents", func(t *testing.T) { 307 + engine := NewSearchEngine(docRepo) 308 + err := engine.Rebuild(ctx) 309 + shared.AssertNoError(t, err, "rebuild should succeed") 310 + 311 + docs, err := engine.Search(ctx, "go", 10) 312 + shared.AssertNoError(t, err, "search should succeed") 313 + shared.AssertTrue(t, len(docs) >= 2, "should find at least 2 documents with 'go'") 314 + }) 315 + 316 + t.Run("returns empty results for non-matching query", func(t *testing.T) { 317 + engine := NewSearchEngine(docRepo) 318 + err := engine.Rebuild(ctx) 319 + shared.AssertNoError(t, err, "rebuild should succeed") 320 + 321 + docs, err := engine.Search(ctx, "rust", 10) 322 + shared.AssertNoError(t, err, "search should succeed") 323 + shared.AssertEqual(t, 0, len(docs), "should return no results for non-matching query") 324 + }) 325 + 326 + t.Run("respects limit parameter", func(t *testing.T) { 327 + engine := NewSearchEngine(docRepo) 328 + err := engine.Rebuild(ctx) 329 + shared.AssertNoError(t, err, "rebuild should succeed") 330 + 331 + docs, err := engine.Search(ctx, "programming", 1) 332 + shared.AssertNoError(t, err, "search should succeed") 333 + shared.AssertTrue(t, len(docs) <= 1, "should respect limit parameter") 334 + }) 335 + 336 + t.Run("returns error with cancelled context", func(t *testing.T) { 337 + engine := NewSearchEngine(docRepo) 338 + err := engine.Rebuild(ctx) 339 + shared.AssertNoError(t, err, "rebuild should succeed") 340 + 341 + canceledCtx := NewCanceledContext() 342 + _, err = engine.Search(canceledCtx, "go", 10) 343 + AssertCancelledContext(t, err) 344 + }) 345 + }) 346 + 347 + t.Run("SearchWithScores", func(t *testing.T) { 348 + t.Run("returns results with scores", func(t *testing.T) { 349 + engine := NewSearchEngine(docRepo) 350 + err := engine.Rebuild(ctx) 351 + shared.AssertNoError(t, err, "rebuild should succeed") 352 + 353 + results, docs, err := engine.SearchWithScores(ctx, "go", 10) 354 + shared.AssertNoError(t, err, "search should succeed") 355 + shared.AssertEqual(t, len(results), len(docs), "results and docs should have same length") 356 + shared.AssertTrue(t, len(results) >= 2, "should find at least 2 results") 357 + 358 + for _, result := range results { 359 + shared.AssertTrue(t, result.Score > 0, "score should be positive") 360 + } 361 + }) 362 + 363 + t.Run("returns error when index not initialized", func(t *testing.T) { 364 + engine := NewSearchEngine(docRepo) 365 + _, _, err := engine.SearchWithScores(ctx, "go", 10) 366 + shared.AssertError(t, err, "should error when index not initialized") 367 + }) 368 + 369 + t.Run("returns error with cancelled context", func(t *testing.T) { 370 + engine := NewSearchEngine(docRepo) 371 + err := engine.Rebuild(ctx) 372 + shared.AssertNoError(t, err, "rebuild should succeed") 373 + 374 + canceledCtx := NewCanceledContext() 375 + _, _, err = engine.SearchWithScores(canceledCtx, "go", 10) 376 + AssertCancelledContext(t, err) 377 + }) 378 + }) 379 + }
+14 -4
internal/repo/queries.go
··· 20 20 ) 21 21 22 22 const ( 23 - taskColumns = "id, uuid, description, status, priority, project, context, tags, due, entry, modified, end, start, annotations, recur, until, parent_uuid" 23 + taskColumns = "id, uuid, description, status, priority, project, context, tags, due, wait, scheduled, entry, modified, end, start, annotations, recur, until, parent_uuid" 24 24 queryTaskByID = "SELECT " + taskColumns + " FROM tasks WHERE id = ?" 25 25 queryTaskByUUID = "SELECT " + taskColumns + " FROM tasks WHERE uuid = ?" 26 26 queryTaskInsert = ` 27 27 INSERT INTO tasks ( 28 28 uuid, description, status, priority, project, context, 29 - tags, due, entry, modified, end, start, annotations, 29 + tags, due, wait, scheduled, entry, modified, end, start, annotations, 30 30 recur, until, parent_uuid 31 31 ) 32 - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` 32 + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` 33 33 queryTaskUpdate = ` 34 34 UPDATE tasks SET 35 35 uuid = ?, description = ?, status = ?, priority = ?, project = ?, context = ?, 36 - tags = ?, due = ?, modified = ?, end = ?, start = ?, annotations = ?, 36 + tags = ?, due = ?, wait = ?, scheduled = ?, modified = ?, end = ?, start = ?, annotations = ?, 37 37 recur = ?, until = ?, parent_uuid = ? 38 38 WHERE id = ?` 39 39 queryTaskDelete = "DELETE FROM tasks WHERE id = ?" 40 40 queryTasksList = "SELECT " + taskColumns + " FROM tasks" 41 + ) 42 + 43 + const ( 44 + documentColumns = "id, title, body, created_at, doc_kind" 45 + queryDocumentByID = "SELECT " + documentColumns + " FROM documents WHERE id = ?" 46 + queryDocumentInsert = `INSERT INTO documents (title, body, created_at, doc_kind) VALUES (?, ?, ?, ?)` 47 + queryDocumentDelete = "DELETE FROM documents WHERE id = ?" 48 + queryDocumentsList = "SELECT " + documentColumns + " FROM documents ORDER BY created_at DESC" 49 + queryDocumentsByKind = "SELECT " + documentColumns + " FROM documents WHERE doc_kind = ? ORDER BY created_at DESC" 50 + queryDocumentsDeleteAll = "DELETE FROM documents" 41 51 ) 42 52 43 53 type scanner interface {
+2
internal/repo/repo.go
··· 20 20 Notes *NoteRepository 21 21 TimeEntries *TimeEntryRepository 22 22 Articles *ArticleRepository 23 + Documents *DocumentRepository 23 24 } 24 25 25 26 // NewRepositories creates a new set of [Repositories] ··· 32 33 Notes: NewNoteRepository(db), 33 34 TimeEntries: NewTimeEntryRepository(db), 34 35 Articles: NewArticleRepository(db), 36 + Documents: NewDocumentRepository(db), 35 37 } 36 38 } 37 39
+124 -6
internal/repo/task_repository.go
··· 4 4 import ( 5 5 "context" 6 6 "database/sql" 7 + "encoding/json" 7 8 "fmt" 8 9 "strings" 9 10 "time" ··· 71 72 if err := s.Scan( 72 73 &task.ID, &task.UUID, &task.Description, &task.Status, &priority, 73 74 &project, &context, &tags, 74 - &task.Due, &task.Entry, &task.Modified, &task.End, &task.Start, &annotations, 75 + &task.Due, &task.Wait, &task.Scheduled, &task.Entry, &task.Modified, &task.End, &task.Start, &annotations, 75 76 &task.Recur, &task.Until, &parentUUID, 76 77 ); err != nil { 77 78 return nil, err ··· 160 161 161 162 result, err := r.db.ExecContext(ctx, queryTaskInsert, 162 163 task.UUID, task.Description, task.Status, task.Priority, task.Project, task.Context, 163 - tags, task.Due, task.Entry, task.Modified, task.End, task.Start, annotations, 164 + tags, task.Due, task.Wait, task.Scheduled, task.Entry, task.Modified, task.End, task.Start, annotations, 164 165 task.Recur, task.Until, task.ParentUUID, 165 166 ) 166 167 if err != nil { ··· 199 200 200 201 // Update modifies an existing task 201 202 func (r *TaskRepository) Update(ctx context.Context, task *models.Task) error { 203 + oldTask, err := r.Get(ctx, task.ID) 204 + if err != nil { 205 + return fmt.Errorf("failed to get current task state: %w", err) 206 + } 207 + 208 + if err := r.SaveHistory(ctx, oldTask, "update"); err != nil { 209 + return fmt.Errorf("failed to save history: %w", err) 210 + } 211 + 202 212 task.Modified = time.Now() 203 213 204 214 tags, err := marshalTaskTags(task) ··· 213 223 214 224 if _, err = r.db.ExecContext(ctx, queryTaskUpdate, 215 225 task.UUID, task.Description, task.Status, task.Priority, task.Project, task.Context, 216 - tags, task.Due, task.Modified, task.End, task.Start, annotations, 226 + tags, task.Due, task.Wait, task.Scheduled, task.Modified, task.End, task.Start, annotations, 217 227 task.Recur, task.Until, task.ParentUUID, 218 228 task.ID, 219 229 ); err != nil { ··· 518 528 func (r *TaskRepository) GetTasksByTag(ctx context.Context, tag string) ([]*models.Task, error) { 519 529 query := ` 520 530 SELECT t.id, t.uuid, t.description, t.status, t.priority, t.project, t.context, 521 - t.tags, t.due, t.entry, t.modified, t.end, t.start, t.annotations, 531 + t.tags, t.due, t.wait, t.scheduled, t.entry, t.modified, t.end, t.start, t.annotations, 522 532 t.recur, t.until, t.parent_uuid 523 533 FROM tasks t, json_each(t.tags) 524 534 WHERE t.tags != '' AND t.tags IS NOT NULL AND json_each.value = ? ··· 684 694 func (r *TaskRepository) GetDependents(ctx context.Context, blockingUUID string) ([]*models.Task, error) { 685 695 query := ` 686 696 SELECT t.id, t.uuid, t.description, t.status, t.priority, t.project, t.context, 687 - t.tags, t.due, t.entry, t.modified, t.end, t.start, t.annotations, t.recur, t.until, t.parent_uuid 697 + t.tags, t.due, t.wait, t.scheduled, t.entry, t.modified, t.end, t.start, t.annotations, t.recur, t.until, t.parent_uuid 688 698 FROM tasks t JOIN task_dependencies d ON t.uuid = d.task_uuid WHERE d.depends_on_uuid = ?` 689 699 690 700 tasks, err := r.queryMany(ctx, query, blockingUUID) ··· 704 714 func (r *TaskRepository) GetBlockedTasks(ctx context.Context, blockingUUID string) ([]*models.Task, error) { 705 715 query := ` 706 716 SELECT t.id, t.uuid, t.description, t.status, t.priority, t.project, t.context, 707 - t.tags, t.due, t.entry, t.modified, t.end, t.start, t.annotations, t.recur, t.until, t.parent_uuid 717 + t.tags, t.due, t.wait, t.scheduled, t.entry, t.modified, t.end, t.start, t.annotations, t.recur, t.until, t.parent_uuid 708 718 FROM tasks t 709 719 JOIN task_dependencies d ON t.uuid = d.task_uuid 710 720 WHERE d.depends_on_uuid = ?` ··· 721 731 } 722 732 return tasks, nil 723 733 } 734 + 735 + // BulkUpdate applies the same updates to multiple tasks 736 + func (r *TaskRepository) BulkUpdate(ctx context.Context, taskIDs []int64, updates *models.Task) error { 737 + if len(taskIDs) == 0 { 738 + return fmt.Errorf("no task IDs provided") 739 + } 740 + 741 + for _, id := range taskIDs { 742 + task, err := r.Get(ctx, id) 743 + if err != nil { 744 + return fmt.Errorf("failed to get task %d: %w", id, err) 745 + } 746 + 747 + if updates.Status != "" { 748 + task.Status = updates.Status 749 + } 750 + if updates.Priority != "" { 751 + task.Priority = updates.Priority 752 + } 753 + if updates.Project != "" { 754 + task.Project = updates.Project 755 + } 756 + if updates.Context != "" { 757 + task.Context = updates.Context 758 + } 759 + if len(updates.Tags) > 0 { 760 + task.Tags = updates.Tags 761 + } 762 + if updates.Due != nil { 763 + task.Due = updates.Due 764 + } 765 + 766 + if err := r.Update(ctx, task); err != nil { 767 + return fmt.Errorf("failed to update task %d: %w", id, err) 768 + } 769 + } 770 + 771 + return nil 772 + } 773 + 774 + // SaveHistory saves a snapshot of a task before an operation 775 + func (r *TaskRepository) SaveHistory(ctx context.Context, task *models.Task, operation string) error { 776 + snapshot, err := json.Marshal(task) 777 + if err != nil { 778 + return fmt.Errorf("failed to marshal task snapshot: %w", err) 779 + } 780 + 781 + query := `INSERT INTO task_history (task_id, operation, snapshot) VALUES (?, ?, ?)` 782 + if _, err := r.db.ExecContext(ctx, query, task.ID, operation, string(snapshot)); err != nil { 783 + return fmt.Errorf("failed to save task history: %w", err) 784 + } 785 + 786 + return nil 787 + } 788 + 789 + // GetHistory retrieves the history of changes for a task 790 + func (r *TaskRepository) GetHistory(ctx context.Context, taskID int64, limit int) ([]*models.TaskHistory, error) { 791 + query := `SELECT id, task_id, operation, snapshot, created_at FROM task_history WHERE task_id = ? ORDER BY created_at DESC` 792 + if limit > 0 { 793 + query += fmt.Sprintf(" LIMIT %d", limit) 794 + } 795 + 796 + rows, err := r.db.QueryContext(ctx, query, taskID) 797 + if err != nil { 798 + return nil, fmt.Errorf("failed to query task history: %w", err) 799 + } 800 + defer rows.Close() 801 + 802 + var history []*models.TaskHistory 803 + for rows.Next() { 804 + h := &models.TaskHistory{} 805 + if err := rows.Scan(&h.ID, &h.TaskID, &h.Operation, &h.Snapshot, &h.CreatedAt); err != nil { 806 + return nil, fmt.Errorf("failed to scan task history: %w", err) 807 + } 808 + history = append(history, h) 809 + } 810 + 811 + return history, rows.Err() 812 + } 813 + 814 + // UndoLastChange reverts a task to its previous state 815 + func (r *TaskRepository) UndoLastChange(ctx context.Context, taskID int64) error { 816 + history, err := r.GetHistory(ctx, taskID, 1) 817 + if err != nil { 818 + return fmt.Errorf("failed to get task history: %w", err) 819 + } 820 + 821 + if len(history) == 0 { 822 + return fmt.Errorf("no history found for task") 823 + } 824 + 825 + lastHistory := history[0] 826 + var task models.Task 827 + if err := json.Unmarshal([]byte(lastHistory.Snapshot), &task); err != nil { 828 + return fmt.Errorf("failed to unmarshal task snapshot: %w", err) 829 + } 830 + 831 + if err := r.Update(ctx, &task); err != nil { 832 + return fmt.Errorf("failed to restore task: %w", err) 833 + } 834 + 835 + deleteQuery := `DELETE FROM task_history WHERE id = ?` 836 + if _, err := r.db.ExecContext(ctx, deleteQuery, lastHistory.ID); err != nil { 837 + return fmt.Errorf("failed to delete history entry: %w", err) 838 + } 839 + 840 + return nil 841 + }
+75
internal/repo/task_repository_test.go
··· 1174 1174 shared.AssertNoError(t, err, "GetBlockedTasks for independent should succeed") 1175 1175 shared.AssertEqual(t, 0, len(emptyBlocked), "independent task should not block anything") 1176 1176 }) 1177 + 1178 + t.Run("BulkUpdate", func(t *testing.T) { 1179 + t.Run("updates multiple tasks successfully", func(t *testing.T) { 1180 + task1 := CreateSampleTask() 1181 + task1.Status = "pending" 1182 + task1.Priority = "low" 1183 + id1, err := repo.Create(ctx, task1) 1184 + shared.AssertNoError(t, err, "Create should succeed") 1185 + 1186 + task2 := CreateSampleTask() 1187 + task2.Status = "pending" 1188 + task2.Priority = "low" 1189 + id2, err := repo.Create(ctx, task2) 1190 + shared.AssertNoError(t, err, "Create should succeed") 1191 + 1192 + updates := &models.Task{ 1193 + Status: "done", 1194 + Priority: "high", 1195 + Project: "bulk-test", 1196 + } 1197 + 1198 + err = repo.BulkUpdate(ctx, []int64{id1, id2}, updates) 1199 + shared.AssertNoError(t, err, "BulkUpdate should succeed") 1200 + 1201 + updated1, err := repo.Get(ctx, id1) 1202 + shared.AssertNoError(t, err, "Get should succeed") 1203 + shared.AssertEqual(t, "done", updated1.Status, "task 1 status should be updated") 1204 + shared.AssertEqual(t, "high", updated1.Priority, "task 1 priority should be updated") 1205 + shared.AssertEqual(t, "bulk-test", updated1.Project, "task 1 project should be updated") 1206 + 1207 + updated2, err := repo.Get(ctx, id2) 1208 + shared.AssertNoError(t, err, "Get should succeed") 1209 + shared.AssertEqual(t, "done", updated2.Status, "task 2 status should be updated") 1210 + shared.AssertEqual(t, "high", updated2.Priority, "task 2 priority should be updated") 1211 + shared.AssertEqual(t, "bulk-test", updated2.Project, "task 2 project should be updated") 1212 + }) 1213 + 1214 + t.Run("fails with no task IDs", func(t *testing.T) { 1215 + updates := &models.Task{Status: "done"} 1216 + err := repo.BulkUpdate(ctx, []int64{}, updates) 1217 + shared.AssertError(t, err, "should fail with empty task IDs") 1218 + shared.AssertContains(t, err.Error(), "no task IDs provided", "error message") 1219 + }) 1220 + 1221 + t.Run("fails with invalid task ID", func(t *testing.T) { 1222 + updates := &models.Task{Status: "done"} 1223 + err := repo.BulkUpdate(ctx, []int64{99999}, updates) 1224 + shared.AssertError(t, err, "should fail with invalid task ID") 1225 + shared.AssertContains(t, err.Error(), "failed to get task", "error message") 1226 + }) 1227 + 1228 + t.Run("updates only non-empty fields", func(t *testing.T) { 1229 + task1 := CreateSampleTask() 1230 + task1.Status = "pending" 1231 + task1.Priority = "low" 1232 + task1.Project = "original-project" 1233 + task1.Context = "original-context" 1234 + id1, err := repo.Create(ctx, task1) 1235 + shared.AssertNoError(t, err, "Create should succeed") 1236 + 1237 + updates := &models.Task{ 1238 + Status: "done", 1239 + } 1240 + 1241 + err = repo.BulkUpdate(ctx, []int64{id1}, updates) 1242 + shared.AssertNoError(t, err, "BulkUpdate should succeed") 1243 + 1244 + updated, err := repo.Get(ctx, id1) 1245 + shared.AssertNoError(t, err, "Get should succeed") 1246 + shared.AssertEqual(t, "done", updated.Status, "status should be updated") 1247 + shared.AssertEqual(t, "low", updated.Priority, "priority should remain unchanged") 1248 + shared.AssertEqual(t, "original-project", updated.Project, "project should remain unchanged") 1249 + shared.AssertEqual(t, "original-context", updated.Context, "context should remain unchanged") 1250 + }) 1251 + }) 1177 1252 }
+178 -125
internal/services/atproto.go
··· 24 24 "github.com/stormlightlabs/noteleaf/internal/public" 25 25 ) 26 26 27 + type MutateRecordOutput struct { 28 + Cid string `json:"cid"` 29 + Uri string `json:"uri"` 30 + } 31 + 27 32 // DocumentWithMeta combines a document with its repository metadata 28 33 type DocumentWithMeta struct { 29 34 Document public.Document 30 35 Meta public.DocumentMeta 31 36 } 32 37 33 - // convertCBORToJSONCompatible recursively converts CBOR data structures to JSON-compatible types 34 - // 35 - // This converts map[any]any to map[string]any to allow usage of [json.Marshal] 36 - func convertCBORToJSONCompatible(data any) any { 37 - switch v := data.(type) { 38 - case map[any]any: 39 - result := make(map[string]any, len(v)) 40 - for key, value := range v { 41 - strKey := fmt.Sprintf("%v", key) 42 - result[strKey] = convertCBORToJSONCompatible(value) 43 - } 44 - return result 45 - case map[string]any: 46 - result := make(map[string]any, len(v)) 47 - for key, value := range v { 48 - result[key] = convertCBORToJSONCompatible(value) 49 - } 50 - return result 51 - case []any: 52 - result := make([]any, len(v)) 53 - for i, item := range v { 54 - result[i] = convertCBORToJSONCompatible(item) 55 - } 56 - return result 57 - default: 58 - return v 59 - } 60 - } 61 - 62 - // convertJSONToCBORCompatible recursively converts JSON-compatible data structures to CBOR types 63 - // 64 - // This converts map[string]any to map[any]any to allow proper CBOR encoding for AT Protocol 65 - func convertJSONToCBORCompatible(data any) any { 66 - switch v := data.(type) { 67 - case map[string]any: 68 - result := make(map[any]any, len(v)) 69 - for key, value := range v { 70 - result[key] = convertJSONToCBORCompatible(value) 71 - } 72 - return result 73 - case map[any]any: 74 - result := make(map[any]any, len(v)) 75 - for key, value := range v { 76 - result[key] = convertJSONToCBORCompatible(value) 77 - } 78 - return result 79 - case []any: 80 - result := make([]any, len(v)) 81 - for i, item := range v { 82 - result[i] = convertJSONToCBORCompatible(item) 83 - } 84 - return result 85 - default: 86 - return v 87 - } 88 - } 89 - 90 38 // PublicationWithMeta combines a publication with its metadata 91 39 type PublicationWithMeta struct { 92 40 Publication public.Publication ··· 117 65 PatchDocument(ctx context.Context, rkey string, doc public.Document, isDraft bool) (*DocumentWithMeta, error) 118 66 DeleteDocument(ctx context.Context, rkey string, isDraft bool) error 119 67 UploadBlob(ctx context.Context, data []byte, mimeType string) (public.Blob, error) 68 + GetDefaultPublication(ctx context.Context) (string, error) 120 69 Close() error 121 70 } 122 71 ··· 127 76 session *Session 128 77 pdsURL string // Personal Data Server URL 129 78 client *xrpc.Client 79 + 80 + // TODO: Future enhancement - integrate OS keychain for secure password storage 81 + // Consider using keyring libraries like: 82 + // - github.com/zalando/go-keyring (cross-platform) 83 + // - keychain access on macOS (Security.framework) 84 + // - Windows Credential Manager (credman) 85 + // - Linux Secret Service API (libsecret) 86 + // This would allow storing app passwords securely in the system keychain 87 + // instead of requiring re-authentication every time JWTs expire. 130 88 } 131 89 132 90 // NewATProtoService creates a new AT Protocol service ··· 195 153 } 196 154 197 155 // RestoreSession restores a previously authenticated session from stored credentials 156 + // and automatically refreshes the token if expired 198 157 func (s *ATProtoService) RestoreSession(session *Session) error { 199 158 if session == nil { 200 159 return fmt.Errorf("session cannot be nil") ··· 218 177 s.client.Host = session.PDSURL 219 178 } 220 179 180 + // Check if token is expired or about to expire (within 5 minutes) 181 + if time.Now().Add(5 * time.Minute).After(session.ExpiresAt) { 182 + ctx := context.Background() 183 + if err := s.RefreshToken(ctx); err != nil { 184 + // Token refresh failed - session may be invalid 185 + // User will need to re-authenticate 186 + return fmt.Errorf("session expired and refresh failed: %w", err) 187 + } 188 + } 189 + 221 190 return nil 222 191 } 223 192 224 193 // RefreshToken refreshes the access token using the refresh token 194 + // This extends the session without requiring the user to re-authenticate 225 195 func (s *ATProtoService) RefreshToken(ctx context.Context) error { 226 196 if s.session == nil || s.session.RefreshJWT == "" { 227 197 return fmt.Errorf("no session available to refresh") ··· 239 209 return fmt.Errorf("failed to refresh session: %w", err) 240 210 } 241 211 212 + // TODO: Consider increasing token lifetime for better UX 213 + // Current: 2 hours - requires frequent re-authentication 214 + // Consider: Store in OS keychain to enable longer sessions without security risk 242 215 expiresAt := time.Now().Add(2 * time.Hour) 243 216 s.session.AccessJWT = output.AccessJwt 244 217 s.session.RefreshJWT = output.RefreshJwt ··· 362 335 return fmt.Errorf("failed to get record bytes for %s: %w", k, err) 363 336 } 364 337 365 - var pub public.Publication 366 - if err := json.Unmarshal(*recordBytes, &pub); err != nil { 367 - return fmt.Errorf("failed to unmarshal publication %s: %w", k, err) 338 + var cborData any 339 + if err := cbor.Unmarshal(*recordBytes, &cborData); err != nil { 340 + return fmt.Errorf("failed to decode CBOR for document %s: %w", k, err) 341 + } 342 + 343 + jsonCompatible := convertCBORToJSONCompatible(cborData) 344 + 345 + jsonBytes, err := json.MarshalIndent(jsonCompatible, "", " ") 346 + if err != nil { 347 + return fmt.Errorf("failed to convert CBOR to JSON for document %s: %w", k, err) 368 348 } 369 349 370 350 parts := strings.Split(k, "/") ··· 373 353 rkey = parts[len(parts)-1] 374 354 } 375 355 356 + var pub public.Publication 357 + if err := json.Unmarshal(jsonBytes, &pub); err != nil { 358 + return fmt.Errorf("failed to unmarshal publication %s: %w", k, err) 359 + } 360 + 376 361 uri := fmt.Sprintf("at://%s/%s", s.session.DID, k) 377 362 publications = append(publications, PublicationWithMeta{ 378 363 Publication: pub, ··· 389 374 return publications, nil 390 375 } 391 376 377 + // GetDefaultPublication returns the URI of the first available publication for the authenticated user 378 + // 379 + // Returns an error if no publications exist 380 + func (s *ATProtoService) GetDefaultPublication(ctx context.Context) (string, error) { 381 + publications, err := s.ListPublications(ctx) 382 + if err != nil { 383 + return "", err 384 + } 385 + 386 + if len(publications) == 0 { 387 + return "", fmt.Errorf("no publications found - create a publication on leaflet.pub first") 388 + } 389 + 390 + return publications[0].URI, nil 391 + } 392 + 392 393 // PostDocument creates a new document in the user's repository 393 394 func (s *ATProtoService) PostDocument(ctx context.Context, doc public.Document, isDraft bool) (*DocumentWithMeta, error) { 394 395 if !s.IsAuthenticated() { ··· 405 406 } 406 407 407 408 doc.Type = collection 408 - 409 409 jsonBytes, err := json.Marshal(doc) 410 410 if err != nil { 411 - return nil, fmt.Errorf("failed to marshal document to JSON: %w", err) 412 - } 413 - 414 - var jsonData map[string]any 415 - if err := json.Unmarshal(jsonBytes, &jsonData); err != nil { 416 - return nil, fmt.Errorf("failed to unmarshal JSON to map: %w", err) 417 - } 418 - 419 - cborCompatible := convertJSONToCBORCompatible(jsonData) 420 - 421 - cborBytes, err := cbor.Marshal(cborCompatible) 422 - if err != nil { 423 - return nil, fmt.Errorf("failed to marshal to CBOR: %w", err) 424 - } 425 - 426 - record := &lexutil.LexiconTypeDecoder{} 427 - if err := cbor.Unmarshal(cborBytes, record); err != nil { 428 - return nil, fmt.Errorf("failed to unmarshal CBOR to lexicon type: %w", err) 411 + return nil, fmt.Errorf("marshal: %w", err) 429 412 } 430 413 431 - input := &atproto.RepoCreateRecord_Input{ 432 - Repo: s.session.DID, 433 - Collection: collection, 434 - Record: record, 414 + var m map[string]any 415 + if err := json.Unmarshal(jsonBytes, &m); err != nil { 416 + return nil, fmt.Errorf("unmarshal: %w", err) 435 417 } 418 + m["$type"] = collection 436 419 437 - output, err := atproto.RepoCreateRecord(ctx, s.client, input) 420 + output, err := repoCreateRecord(ctx, s.client, s.session.DID, collection, m) 438 421 if err != nil { 439 422 return nil, fmt.Errorf("failed to create record: %w", err) 440 423 } 441 424 442 425 parts := strings.Split(output.Uri, "/") 443 - rkey := "" 444 - if len(parts) > 0 { 445 - rkey = parts[len(parts)-1] 446 - } 447 - 426 + rkey := parts[len(parts)-1] 448 427 meta := public.DocumentMeta{ 449 428 RKey: rkey, 450 429 CID: output.Cid, ··· 452 431 IsDraft: isDraft, 453 432 FetchedAt: time.Now(), 454 433 } 455 - 456 - return &DocumentWithMeta{ 457 - Document: doc, 458 - Meta: meta, 459 - }, nil 434 + return &DocumentWithMeta{Document: doc, Meta: meta}, nil 460 435 } 461 436 462 437 // PatchDocument updates an existing document in the user's repository ··· 479 454 } 480 455 481 456 doc.Type = collection 482 - 483 457 jsonBytes, err := json.Marshal(doc) 484 458 if err != nil { 485 - return nil, fmt.Errorf("failed to marshal document to JSON: %w", err) 486 - } 487 - 488 - var jsonData map[string]any 489 - if err := json.Unmarshal(jsonBytes, &jsonData); err != nil { 490 - return nil, fmt.Errorf("failed to unmarshal JSON to map: %w", err) 491 - } 492 - 493 - cborCompatible := convertJSONToCBORCompatible(jsonData) 494 - 495 - cborBytes, err := cbor.Marshal(cborCompatible) 496 - if err != nil { 497 - return nil, fmt.Errorf("failed to marshal to CBOR: %w", err) 459 + return nil, fmt.Errorf("marshal: %w", err) 498 460 } 499 461 500 - record := &lexutil.LexiconTypeDecoder{} 501 - if err := cbor.Unmarshal(cborBytes, record); err != nil { 502 - return nil, fmt.Errorf("failed to unmarshal CBOR to lexicon type: %w", err) 503 - } 504 - 505 - input := &atproto.RepoPutRecord_Input{ 506 - Repo: s.session.DID, 507 - Collection: collection, 508 - Rkey: rkey, 509 - Record: record, 462 + var m map[string]any 463 + if err := json.Unmarshal(jsonBytes, &m); err != nil { 464 + return nil, fmt.Errorf("unmarshal: %w", err) 510 465 } 466 + m["$type"] = collection 511 467 512 - output, err := atproto.RepoPutRecord(ctx, s.client, input) 468 + output, err := repoPutRecord(ctx, s.client, s.session.DID, collection, rkey, m) 513 469 if err != nil { 514 470 return nil, fmt.Errorf("failed to update record: %w", err) 515 471 } 516 472 517 473 uri := fmt.Sprintf("at://%s/%s/%s", s.session.DID, collection, rkey) 518 - 519 474 meta := public.DocumentMeta{ 520 475 RKey: rkey, 521 476 CID: output.Cid, ··· 523 478 IsDraft: isDraft, 524 479 FetchedAt: time.Now(), 525 480 } 526 - 527 - return &DocumentWithMeta{ 528 - Document: doc, 529 - Meta: meta, 530 - }, nil 481 + return &DocumentWithMeta{Document: doc, Meta: meta}, nil 531 482 } 532 483 533 484 // DeleteDocument removes a document from the user's repository ··· 592 543 s.session = nil 593 544 return nil 594 545 } 546 + 547 + func repoCreateRecord(ctx context.Context, client *xrpc.Client, repo, collection string, record map[string]any) (*MutateRecordOutput, error) { 548 + body := map[string]any{ 549 + "repo": repo, 550 + "collection": collection, 551 + "record": record, 552 + } 553 + 554 + var out MutateRecordOutput 555 + if err := client.LexDo( 556 + ctx, 557 + lexutil.Procedure, 558 + "application/json", 559 + "com.atproto.repo.createRecord", 560 + nil, 561 + body, 562 + &out, 563 + ); err != nil { 564 + return nil, fmt.Errorf("repoCreateRecord failed: %w", err) 565 + } 566 + return &out, nil 567 + } 568 + 569 + func repoPutRecord(ctx context.Context, client *xrpc.Client, repo, collection, rkey string, record map[string]any) (*MutateRecordOutput, error) { 570 + body := map[string]any{ 571 + "repo": repo, 572 + "collection": collection, 573 + "rkey": rkey, 574 + "record": record, 575 + } 576 + 577 + var out MutateRecordOutput 578 + if err := client.LexDo( 579 + ctx, 580 + lexutil.Procedure, 581 + "application/json", 582 + "com.atproto.repo.putRecord", 583 + nil, 584 + body, 585 + &out, 586 + ); err != nil { 587 + return nil, fmt.Errorf("repoPutRecord failed: %w", err) 588 + } 589 + return &out, nil 590 + } 591 + 592 + // convertCBORToJSONCompatible recursively converts CBOR data structures to JSON-compatible types 593 + // 594 + // This converts map[any]any to map[string]any to allow usage of [json.Marshal] 595 + func convertCBORToJSONCompatible(data any) any { 596 + switch v := data.(type) { 597 + case map[any]any: 598 + result := make(map[string]any, len(v)) 599 + for key, value := range v { 600 + strKey := fmt.Sprintf("%v", key) 601 + result[strKey] = convertCBORToJSONCompatible(value) 602 + } 603 + return result 604 + case map[string]any: 605 + result := make(map[string]any, len(v)) 606 + for key, value := range v { 607 + result[key] = convertCBORToJSONCompatible(value) 608 + } 609 + return result 610 + case []any: 611 + result := make([]any, len(v)) 612 + for i, item := range v { 613 + result[i] = convertCBORToJSONCompatible(item) 614 + } 615 + return result 616 + default: 617 + return v 618 + } 619 + } 620 + 621 + // convertJSONToCBORCompatible recursively converts JSON-compatible data structures to CBOR types 622 + // 623 + // This converts map[string]any to map[any]any to allow proper CBOR encoding for AT Protocol 624 + func convertJSONToCBORCompatible(data any) any { 625 + switch v := data.(type) { 626 + case map[string]any: 627 + result := make(map[any]any, len(v)) 628 + for key, value := range v { 629 + result[key] = convertJSONToCBORCompatible(value) 630 + } 631 + return result 632 + case map[any]any: 633 + result := make(map[any]any, len(v)) 634 + for key, value := range v { 635 + result[key] = convertJSONToCBORCompatible(value) 636 + } 637 + return result 638 + case []any: 639 + result := make([]any, len(v)) 640 + for i, item := range v { 641 + result[i] = convertJSONToCBORCompatible(item) 642 + } 643 + return result 644 + default: 645 + return v 646 + } 647 + }
+88 -5
internal/services/atproto_test.go
··· 506 506 }) 507 507 }) 508 508 509 + t.Run("GetDefaultPublication", func(t *testing.T) { 510 + t.Run("returns error when not authenticated", func(t *testing.T) { 511 + svc := NewATProtoService() 512 + ctx := context.Background() 513 + 514 + uri, err := svc.GetDefaultPublication(ctx) 515 + if err == nil { 516 + t.Error("Expected error when getting default publication without authentication") 517 + } 518 + if uri != "" { 519 + t.Errorf("Expected empty URI, got %s", uri) 520 + } 521 + if !strings.Contains(err.Error(), "not authenticated") { 522 + t.Errorf("Expected 'not authenticated' error, got '%v'", err) 523 + } 524 + }) 525 + 526 + t.Run("returns error when session not authenticated", func(t *testing.T) { 527 + svc := NewATProtoService() 528 + ctx := context.Background() 529 + svc.session = &Session{ 530 + Handle: "test.bsky.social", 531 + Authenticated: false, 532 + } 533 + 534 + uri, err := svc.GetDefaultPublication(ctx) 535 + if err == nil { 536 + t.Error("Expected error when getting default publication with unauthenticated session") 537 + } 538 + if uri != "" { 539 + t.Errorf("Expected empty URI, got %s", uri) 540 + } 541 + }) 542 + 543 + t.Run("returns error when no publications exist", func(t *testing.T) { 544 + svc := NewATProtoService() 545 + svc.session = &Session{ 546 + DID: "did:plc:test123", 547 + Handle: "test.bsky.social", 548 + AccessJWT: "access_token", 549 + RefreshJWT: "refresh_token", 550 + Authenticated: true, 551 + } 552 + ctx := context.Background() 553 + 554 + _, err := svc.GetDefaultPublication(ctx) 555 + if err == nil { 556 + t.Error("Expected error when getting default publication") 557 + } 558 + // With invalid credentials, we expect either auth error or no publications error 559 + if !strings.Contains(err.Error(), "no publications found") && 560 + !strings.Contains(err.Error(), "Authentication") && 561 + !strings.Contains(err.Error(), "AuthMissing") && 562 + !strings.Contains(err.Error(), "failed to fetch repository") { 563 + t.Errorf("Expected authentication or 'no publications found' error, got '%v'", err) 564 + } 565 + }) 566 + 567 + t.Run("returns error when context cancelled", func(t *testing.T) { 568 + svc := NewATProtoService() 569 + svc.session = &Session{ 570 + DID: "did:plc:test123", 571 + Handle: "test.bsky.social", 572 + AccessJWT: "access_token", 573 + RefreshJWT: "refresh_token", 574 + Authenticated: true, 575 + } 576 + 577 + ctx, cancel := context.WithCancel(context.Background()) 578 + cancel() 579 + 580 + uri, err := svc.GetDefaultPublication(ctx) 581 + if err == nil { 582 + t.Error("Expected error when context is cancelled") 583 + } 584 + if uri != "" { 585 + t.Errorf("Expected empty URI when error occurs, got %s", uri) 586 + } 587 + }) 588 + }) 589 + 509 590 t.Run("Authentication Error Scenarios", func(t *testing.T) { 510 591 t.Run("returns error with context timeout", func(t *testing.T) { 511 592 svc := NewATProtoService() ··· 1153 1234 defaultPDSURL := svc.pdsURL 1154 1235 1155 1236 session := &Session{ 1156 - DID: "did:plc:test123", 1157 - Handle: "test.bsky.social", 1158 - AccessJWT: "access_token", 1159 - RefreshJWT: "refresh_token", 1160 - PDSURL: "", 1237 + DID: "did:plc:test123", 1238 + Handle: "test.bsky.social", 1239 + AccessJWT: "access_token", 1240 + RefreshJWT: "refresh_token", 1241 + PDSURL: "", 1242 + ExpiresAt: time.Now().Add(2 * time.Hour), 1243 + Authenticated: true, 1161 1244 } 1162 1245 1163 1246 err := svc.RestoreSession(session)
+1 -1
internal/services/samples/movie.html
··· 1 - <!DOCTYPE html><html lang="en" dir="ltr" xmlns="http://www.w3.org/1999/xhtml" prefix="fb: http://www.facebook.com/2008/fbml og: http://opengraphprotocol.org/schema/"><head prefix="og: http://ogp.me/ns# flixstertomatoes: http://ogp.me/ns/apps/flixstertomatoes#"><script charset="UTF-8" crossorigin="anonymous" data-domain-script="7e979733-6841-4fce-9182-515fac69187f" integrity="sha384-TKdmlzVmoD70HzftTw4WtOzIBL5mNx8mXSRzEvwrWjpIJ7FZ/EuX758yMDWXtRUN" src="https://cdn.cookielaw.org/consent/7e979733-6841-4fce-9182-515fac69187f/otSDKStub.js" type="text/javascript"></script><script type="text/javascript">function OptanonWrapper(){ if (OnetrustActiveGroups.includes('7')){ document.querySelector('search-results-nav-manager')?.setAlgoliaInsightUserToken?.();}} </script><script ccpa-opt-out-ids="USP" ccpa-opt-out-geo="US" ccpa-opt-out-lspa="false" charset="UTF-8" src="https://cdn.cookielaw.org/opt-out/otCCPAiab.js" type="text/javascript"></script><script src="/assets/pizza-pie/javascripts/bundles/roma/rt-common.js?single"></script><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><meta http-equiv="x-ua-compatible" content="ie=edge"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="shortcut icon" sizes="76x76" type="image/x-icon" href="https://www.rottentomatoes.com/assets/pizza-pie/images/favicon.ico" /><title>The Fantastic Four: First Steps | Rotten Tomatoes</title><meta name="description" content="Discover reviews, ratings, and trailers for The Fantastic Four: First Steps on Rotten Tomatoes. Stay updated with critic and audience scores today!" /><meta name="twitter:card" content="summary" /><meta name="twitter:image" content="https://resizing.flixster.com/GRRDF-MY6_iS5Em5vNBg-Jd-uL0=/206x305/v2/https://resizing.flixster.com/WEfe-vTCqjHioD77J7f-qeoZZdY=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2FjN2M2NzA0LWNiMTctNDhjMy05N2VlLTk3YWI3N2JhZDZmMS5qcGc=" /><meta name="twitter:title" content="The Fantastic Four: First Steps | Rotten Tomatoes" /><meta name="twitter:text:title" content="The Fantastic Four: First Steps | Rotten Tomatoes" /><meta name="twitter:description" content="Discover reviews, ratings, and trailers for The Fantastic Four: First Steps on Rotten Tomatoes. Stay updated with critic and audience scores today!" /><meta property="og:site_name" content="Rotten Tomatoes" /><meta property="og:title" content="The Fantastic Four: First Steps | Rotten Tomatoes" /><meta property="og:description" content="Discover reviews, ratings, and trailers for The Fantastic Four: First Steps on Rotten Tomatoes. Stay updated with critic and audience scores today!" /><meta property="og:type" content="video.movie" /><meta property="og:url" content="https://www.rottentomatoes.com/m/the_fantastic_four_first_steps" /><meta property="og:image" content="https://resizing.flixster.com/GRRDF-MY6_iS5Em5vNBg-Jd-uL0=/206x305/v2/https://resizing.flixster.com/WEfe-vTCqjHioD77J7f-qeoZZdY=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2FjN2M2NzA0LWNiMTctNDhjMy05N2VlLTk3YWI3N2JhZDZmMS5qcGc=" /><meta property="og:locale" content="en_US" /><link rel="canonical" href="https://www.rottentomatoes.com/m/the_fantastic_four_first_steps" /><script>var dataLayer=dataLayer || []; var RottenTomatoes=RottenTomatoes ||{}; RottenTomatoes.dtmData={ "customerId": "", "loggedInStatus": "", "rtVersion": 3.1, "webVersion": "node", "emsID": "db64beee-683b-39ce-9617-94f6b67aa997", "lifeCycleWindow": "IN_THEATERS", "pageName": "rt | movies | overview | The Fantastic Four: First Steps", "titleGenre": "Action", "titleId": "db64beee-683b-39ce-9617-94f6b67aa997", "titleName": "The Fantastic Four: First Steps", "titleType": "Movie"}; dataLayer.push({ "customerId": "", "loggedInStatus": "", "rtVersion": 3.1, "webVersion": "node", "emsID": "db64beee-683b-39ce-9617-94f6b67aa997", "lifeCycleWindow": "IN_THEATERS", "pageName": "rt | movies | overview | The Fantastic Four: First Steps", "titleGenre": "Action", "titleId": "db64beee-683b-39ce-9617-94f6b67aa997", "titleName": "The Fantastic Four: First Steps", "titleType": "Movie"}); </script><script id="mps-page-integration">window.mpscall={ "cag[certified_fresh]": "0", "cag[fresh_rotten]": "rotten", "cag[genre]": "Action|Adventure|Sci-Fi|Fantasy", "cag[movieshow]": "The Fantastic Four: First Steps", "cag[rating]": "PG-13", "cag[release]": "Jul 25, 2025", "cag[score]": "null", "cag[urlid]": "/the_fantastic_four_first_steps", "cat": "movie|movie_page", "field[env]": "production", "field[rtid]": "db64beee-683b-39ce-9617-94f6b67aa997", "title": "The Fantastic Four: First Steps", "type": "movie_page", "site": "rottentomatoes-web"}; var mpsopts={ 'host': 'mps.nbcuni.com', 'updatecorrelator': 1}; var mps=mps ||{}; mps._ext=mps._ext ||{}; mps._adsheld=[]; mps._queue=mps._queue ||{}; mps._queue.mpsloaded=mps._queue.mpsloaded || []; mps._queue.mpsinit=mps._queue.mpsinit || []; mps._queue.gptloaded=mps._queue.gptloaded || []; mps._queue.adload=mps._queue.adload || []; mps._queue.adclone=mps._queue.adclone || []; mps._queue.adview=mps._queue.adview || []; mps._queue.refreshads=mps._queue.refreshads || []; mps.__timer=Date.now || function (){ return +new Date}; mps.__intcode="v2"; if (typeof mps.getAd !="function") mps.getAd=function (adunit){ if (typeof adunit !="string") return false; var slotid="mps-getad-" + adunit.replace(/\W/g, ""); if (!mps._ext || !mps._ext.loaded){ mps._queue.gptloaded.push(function (){ typeof mps._gptfirst=="function" && mps._gptfirst(adunit, slotid); mps.insertAd("#" + slotid, adunit)}); mps._adsheld.push(adunit)} return '<div id="' + slotid + '" class="mps-wrapper" data-mps-fill-slot="' + adunit + '"></div>'}; </script><script src="//mps.nbcuni.com/fetch/ext/load-rottentomatoes-web.js?nowrite=2" id="mps-load"></script><script type="application/ld+json">{"@context":"http://schema.org","@type":"Movie","actor":[{"@type":"Person","name":"Pedro Pascal","sameAs":"https://www.rottentomatoes.com/celebrity/pedro_pascal","image":"https://resizing.flixster.com/gHGci208eTBBe6s555qqrVvMAlg=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/494807_v9_bd.jpg"},{"@type":"Person","name":"Vanessa Kirby","sameAs":"https://www.rottentomatoes.com/celebrity/vanessa_kirby","image":"https://resizing.flixster.com/hYfFokzVjtDj7QBvMrYIJ5uAhmY=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/631337_v9_bb.jpg"},{"@type":"Person","name":"Ebon Moss-Bachrach","sameAs":"https://www.rottentomatoes.com/celebrity/ebon_moss_bachrach","image":"https://resizing.flixster.com/cEb3kX_Yn_L4trv76D6rNON5nLA=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/252022_v9_bc.jpg"},{"@type":"Person","name":"Joseph Quinn","sameAs":"https://www.rottentomatoes.com/celebrity/joseph_quinn","image":"https://resizing.flixster.com/mwGK9WCbDyzD9FKvC81OyaWTfjc=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/1102755_v9_bb.jpg"},{"@type":"Person","name":"Ralph Ineson","sameAs":"https://www.rottentomatoes.com/celebrity/ralph-ineson","image":"https://resizing.flixster.com/-UyEiZ3UKHhGzdQU4wDV10Z6wO0=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/268419_v9_bc.jpg"}],"aggregateRating":{"@type":"AggregateRating","bestRating":"100","description":"The Tomatometer rating โ€“ based on the published opinions of hundreds of film and television critics โ€“ is a trusted measurement of movie and TV programming quality for millions of moviegoers. It represents the percentage of professional critic reviews that are positive for a given film or television show.","name":"Tomatometer","ratingCount":390,"ratingValue":"87","reviewCount":390,"worstRating":"0"},"contentRating":"PG-13","dateCreated":"2025-07-25","director":[{"@type":"Person","name":"Matt Shakman","sameAs":"https://www.rottentomatoes.com/celebrity/matt-shakman","image":"https://images.fandango.com/cms/assets/b0cefeb0-b6a8-11ed-81d8-51a487a38835--poster-default-thumbnail.jpg"}],"description":"Discover reviews, ratings, and trailers for The Fantastic Four: First Steps on Rotten Tomatoes. Stay updated with critic and audience scores today!","genre":["Action","Adventure","Sci-Fi","Fantasy"],"image":"https://resizing.flixster.com/WEfe-vTCqjHioD77J7f-qeoZZdY=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2FjN2M2NzA0LWNiMTctNDhjMy05N2VlLTk3YWI3N2JhZDZmMS5qcGc=","name":"The Fantastic Four: First Steps","url":"https://www.rottentomatoes.com/m/the_fantastic_four_first_steps","video":{"@type":"VideoObject","thumbnailUrl":"https://statcdn.fandango.com/MPX/image/NBCU_Fandango/663/931/thumb_4ca6d6c5-6a62-11f0-94b5-022bbbb30d69.jpg","name":"The Fantastic Four: First Steps: 4 Us All","duration":"0:59","sourceOrganization":"MPX","uploadDate":"2025-07-26T20:55:44","description":"","contentUrl":"https://www.rottentomatoes.com/m/the_fantastic_four_first_steps/videos/BBgYzBvAIQDN"}}</script><link rel="manifest" href="https://www.rottentomatoes.com/assets/pizza-pie/manifest/manifest.json" /><link rel="apple-touch-icon" href="https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/apple-touch-icon-60.jpg" /><link rel="apple-touch-icon" sizes="152x152" href="https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/apple-touch-icon-152.jpg" /><link rel="apple-touch-icon" sizes="167x167" href="https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/apple-touch-icon-167.jpg" /><link rel="apple-touch-icon" sizes="180x180" href="https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/apple-touch-icon-180.jpg" /><meta name="apple-itunes-app" content="app-id=6673916573, app-argument=https://www.rottentomatoes.com/"><meta name="google-site-verification" content="VPPXtECgUUeuATBacnqnCm4ydGO99reF-xgNklSbNbc" /><meta name="msvalidate.01" content="034F16304017CA7DCF45D43850915323" /><meta name="theme-color" content="#FA320A"><meta http-equiv="x-dns-prefetch-control" content="on"><link rel="dns-prefetch" href="//www.rottentomatoes.com" /><link rel="preconnect" href="//www.rottentomatoes.com" /><link rel="stylesheet" href="/assets/pizza-pie/stylesheets/bundles/layouts/default.b6077682d41.css" /><link rel="preload" href="/assets/pizza-pie/stylesheets/bundles/pages/movie.802363b9d93.css" as="style" onload="this.onload=null;this.rel='stylesheet'" /><script>window.RottenTomatoes={}; window.RTLocals={}; window.nunjucksPrecompiled={}; window.__RT__={}; </script><script src="https://cdn.jwplayer.com/libraries/U8MHzHHR.js"></script><script src="https://sb.scorecardresearch.com/c2/plugins/streamingtag_plugin_jwplayer.js"></script><script>!function (e){ var n="https://s.go-mpulse.net/boomerang/"; if ("False"=="True") e.BOOMR_config=e.BOOMR_config ||{}, e.BOOMR_config.PageParams=e.BOOMR_config.PageParams ||{}, e.BOOMR_config.PageParams.pci=!0, n="https://s2.go-mpulse.net/boomerang/"; if (window.BOOMR_API_key="4RDDZ-2Z6GP-RRNMC-PYEUL-SK6K9", function (){ function e(){ if (!o){ var e=document.createElement("script"); e.id="boomr-scr-as", e.src=window.BOOMR.url, e.async=!0, i.parentNode.appendChild(e), o=!0}} function t(e){ o=!0; var n, t, a, r, d=document, O=window; if (window.BOOMR.snippetMethod=e ? "if" : "i", t=function (e, n){ var t=d.createElement("script"); t.id=n || "boomr-if-as", t.src=window.BOOMR.url, BOOMR_lstart=(new Date).getTime(), e=e || d.body, e.appendChild(t)}, !window.addEventListener && window.attachEvent && navigator.userAgent.match(/MSIE [67]\./)) return window.BOOMR.snippetMethod="s", void t(i.parentNode, "boomr-async"); a=document.createElement("IFRAME"), a.src="about:blank", a.title="", a.role="presentation", a.loading="eager", r=(a.frameElement || a).style, r.width=0, r.height=0, r.border=0, r.display="none", i.parentNode.appendChild(a); try{ O=a.contentWindow, d=O.document.open()} catch (_){ n=document.domain, a.src="javascript:var d=document.open();d.domain='" + n + "';void(0);", O=a.contentWindow, d=O.document.open()} if (n) d._boomrl=function (){ this.domain=n, t()}, d.write("<bo" + "dy onload='document._boomrl();'>"); else if (O._boomrl=function (){ t()}, O.addEventListener) O.addEventListener("load", O._boomrl, !1); else if (O.attachEvent) O.attachEvent("onload", O._boomrl); d.close()} function a(e){ window.BOOMR_onload=e && e.timeStamp || (new Date).getTime()} if (!window.BOOMR || !window.BOOMR.version && !window.BOOMR.snippetExecuted){ window.BOOMR=window.BOOMR ||{}, window.BOOMR.snippetStart=(new Date).getTime(), window.BOOMR.snippetExecuted=!0, window.BOOMR.snippetVersion=12, window.BOOMR.url=n + "4RDDZ-2Z6GP-RRNMC-PYEUL-SK6K9"; var i=document.currentScript || document.getElementsByTagName("script")[0], o=!1, r=document.createElement("link"); if (r.relList && "function"==typeof r.relList.supports && r.relList.supports("preload") && "as" in r) window.BOOMR.snippetMethod="p", r.href=window.BOOMR.url, r.rel="preload", r.as="script", r.addEventListener("load", e), r.addEventListener("error", function (){ t(!0)}), setTimeout(function (){ if (!o) t(!0)}, 3e3), BOOMR_lstart=(new Date).getTime(), i.parentNode.appendChild(r); else t(!1); if (window.addEventListener) window.addEventListener("load", a, !1); else if (window.attachEvent) window.attachEvent("onload", a)}}(), "".length >0) if (e && "performance" in e && e.performance && "function"==typeof e.performance.setResourceTimingBufferSize) e.performance.setResourceTimingBufferSize(); !function (){ if (BOOMR=e.BOOMR ||{}, BOOMR.plugins=BOOMR.plugins ||{}, !BOOMR.plugins.AK){ var n=""=="true" ? 1 : 0, t="", a="eyd6zaauaeceajqacqcoyaaafful3mml-f-5c35fc32a-clienttons-s.akamaihd.net", i="false"=="true" ? 2 : 1, o={ "ak.v": "39", "ak.cp": "1839344", "ak.ai": parseInt("1226367", 10), "ak.ol": "0", "ak.cr": 18, "ak.ipv": 6, "ak.proto": "h2", "ak.rid": "1bdfb774", "ak.r": 43883, "ak.a2": n, "ak.m": "", "ak.n": "essl", "ak.bpcip": "2607:ec80:1401:440::", "ak.cport": 63434, "ak.gh": "23.205.103.141", "ak.quicv": "", "ak.tlsv": "tls1.3", "ak.0rtt": "", "ak.0rtt.ed": "", "ak.csrc": "-", "ak.acc": "", "ak.t": "1757262219", "ak.ak": "hOBiQwZUYzCg5VSAfCLimQ==CKkYzF1l8fCYAQDO3Ka6MQjEJqqg+HMg9vMLYhzlSa+AgTCzDOkKjEMkU7+MkMRwJYAGkOLlLS+jnjLsXEZsaUvIxS8WR/6oulucAmT5EAp/gQxgkVlyn7CkRoTR1qngs9nfWRHFap8G/kos+Xn2LaKq6rHdrS0emZBD0M+6AwFkRkVNOeqCVAxEgKGTIimmHJVcLgr7d4Wa0fDhrGbN5kU1qjpubofpGYnQ9YOoFzyWknnn/I++11yvnp8VSq5PxT5slvOiDYd8WjunbJMe2vTEwzj9TZgPYz0EFHqOnmZka3snkbsTEMw0tF7X1vboLEVOPIZccve4Wx6QJK92m8GR3ZcVQD5M1W7VXT8TJzEYsfGhLmyFSFwJLxglNwKb0mqAzlOuTnMp0hzMU2Cl3lpF+GXLOxdS40tXPD8j1kg=", "ak.pv": "3", "ak.dpoabenc": "", "ak.tf": i}; if ("" !==t) o["ak.ruds"]=t; var r={ i: !1, av: function (n){ var t="http.initiator"; if (n && (!n[t] || "spa_hard"===n[t])) o["ak.feo"]=void 0 !==e.aFeoApplied ? 1 : 0, BOOMR.addVar(o)}, rv: function (){ var e=["ak.bpcip", "ak.cport", "ak.cr", "ak.csrc", "ak.gh", "ak.ipv", "ak.m", "ak.n", "ak.ol", "ak.proto", "ak.quicv", "ak.tlsv", "ak.0rtt", "ak.0rtt.ed", "ak.r", "ak.acc", "ak.t", "ak.tf"]; BOOMR.removeVar(e)}}; BOOMR.plugins.AK={ akVars: o, akDNSPreFetchDomain: a, init: function (){ if (!r.i){ var e=BOOMR.subscribe; e("before_beacon", r.av, null, null), e("onbeacon", r.rv, null, null), r.i=!0} return this}, is_complete: function (){ return !0}}}}()}(window);</script></head><body class="body no-touch js-mptd-layout" data-AdsGlobalSkinTakeoverManager="body" data-SearchResultsNavManager="body"><cookie-manager></cookie-manager><device-inspection-manager endpoint="https://www.rottentomatoes.com/napi/device/inspection"></device-inspection-manager><user-activity-manager profiles-features-enabled="false"></user-activity-manager><user-identity-manager profiles-features-enabled="false"></user-identity-manager><ad-unit-manager></ad-unit-manager><auth-initiate-manager profiles-username-enabled="false" data-ArtiManager="authInitiateManager" data-WatchlistButtonManager="authInitiateManager:createAccount"></auth-initiate-manager><auth-profile-manager data-AuthInitiateManager="authProfileManager"></auth-profile-manager><auth-validation-manager data-AuthInitiateManager="authValidation"></auth-validation-manager><overlay-base data-AuthInitiateManager="overlayBase:close" data-PagePollsIndexManager="authOverlay:close" hidden><overlay-flows data-AuthInitiateManager="overlayFlows" slot="content"><action-icon slot="close" class="auth-overlay__icon-button auth-overlay__icon-button--close" aria-label="Close" data-qa="close-overlay-btn" icon="close"></action-icon></overlay-flows></overlay-base><notification-alert data-AuthInitiateManager="authSuccess" animate hidden><rt-icon icon="check-circled"></rt-icon><span>Signed in</span></notification-alert><div id="auth-templates" data-AuthInitiateManager="authTemplates"><template slot="screens" id="account-create-username-screen"><account-create-username-screen data-qa="account-create-username-screen"><input-label slot="input-username" state="default" data-qa="username-input-label"><label slot="label" for="create-username-input">Username</label><input slot="input" id="create-username-input" type="text" placeholder="Username" data-qa="username-input" /></input-label><rt-button disabled slot="btn-continue" shape="pill" data-qa="continue-btn">Continue</rt-button><rt-text class="terms-and-policies" slot="terms-and-policies" size="0.75">By joining, you agree to the <rt-link href="/policies/terms-and-policies" target="_blank" data-qa="terms-policies-link">Terms and Policies</rt-link>and the <rt-link href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" data-qa="privacy-policy-link">Privacy Policy</rt-link>and to receive email from <rt-link href="https://www.fandango.com/about-us" target="_blank" data-qa="about-fandango-link">Fandango Media Brands</rt-link>. </rt-text></account-create-username-screen></template><template slot="screens" id="account-email-change-screen"><account-email-change-screen data-qa="account-email-change-screen" email="user@email.com"><input-label class="new-email-input" state="default" slot="new-email-input" data-qa="email-input-label"><label slot="label" for="newEmail">Enter new email</label><input slot="input" name="newEmail" type="text" placeholder="Enter new email" autocomplete="off" data-qa="email-input"></input></input-label><rt-button slot="submit-button" disabled shape="pill" data-qa="submit-btn">Submit</rt-button><rt-text class="terms-and-policies" slot="terms-and-policies" size="0.75">By joining, you agree to the <rt-link href="/policies/terms-and-policies" target="_blank" data-qa="terms-policies-link">Terms and Policies</rt-link>and <rt-link href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" data-qa="privacy-policy-link">Privacy Policy</rt-link>and to receive email from the <rt-link href="https://www.fandango.com/about-us" target="_blank" data-qa="about-fandango-link">Fandango Media Brands</rt-link>. </rt-text></account-email-change-screen></template><template slot="screens" id="account-email-change-success-screen"><account-email-change-success-screen data-qa="login-create-success-screen"><rt-text slot="message" size="1.5" style="--fontWeight: var(--franklinGothicDemi);">Email change successful</rt-text><rt-text slot="submessage">You are signed out for your security. </br>Please sign in again.</rt-text><img slot="icon" src="/assets/pizza-pie/images/icons/cognito-success.3a3d5f32fab.svg" width="111" height="105" /></account-email-change-success-screen></template><template slot="screens" id="account-password-change-screen"><account-password-change-screen data-qa="account-password-change-screen"><input-label state="default" slot="input-password-existing"><label slot="label" for="password-existing">Existing password</label><input slot="input" name="password-existing" type="password" placeholder="Enter existing password" autocomplete="off"></input></input-label><input-label state="default" slot="input-password-new"><label slot="label" for="password-new">New password</label><input slot="input" name="password-new" type="password" placeholder="Enter new password" autocomplete="off"></input></input-label><rt-button disabled shape="pill" slot="submit-button">Submit</rt-button></account-password-change-screen></template><template slot="screens" id="account-password-change-updating-screen"><login-success-screen data-qa="account-password-change-updating-screen" hidebanner><rt-text slot="status" size="1.5" style="--fontWeight: var(--franklinGothicDemi);">Updating your password... </rt-text><img slot="icon" src="/assets/pizza-pie/images/icons/cognito-success.3a3d5f32fab.svg" width="111" height="105" /></login-success-screen></template><template slot="screens" id="account-password-change-success-screen"><login-success-screen data-qa="account-password-change-success-screen" hidebanner><rt-text slot="status" size="1.5" style="--fontWeight: var(--franklinGothicDemi);">Success! </rt-text><img slot="icon" src="/assets/pizza-pie/images/icons/cognito-success.3a3d5f32fab.svg" width="111" height="105" /></login-success-screen></template><template slot="screens" id="account-verifying-email-screen"><account-verifying-email-screen data-qa="account-verifying-email-screen"><img slot="icon" src="/assets/pizza-pie/images/icons/cognito-auth-verify.e74a69e9a77.svg" alt="email" /><rt-text slot="status">Verifying your email... </rt-text><rt-button type="cta-large" href="/?authFlowScreen=loginStartScreen" slot="retryLink" size="0.875" style="--fontWeight: var(--franklinGothicMedium);" data-qa="retry-link">Retry </rt-button></account-verifying-email-screen></template><template slot="screens" id="cognito-loading"><div><loading-spinner id="cognito-auth-loading-spinner"></loading-spinner><style>#cognito-auth-loading-spinner{ font-size: 2rem; transform: translate(calc(100% - 1em), 250px); width: 50%;} </style></div></template><template slot="screens" id="login-check-email-screen"><login-check-email-screen data-qa="login-check-email-screen" email="user@email.com"><rt-text class="note-text" size="1" slot="noteText">Please open the email link from the same browser you initiated the change email process from. </rt-text><rt-text slot="gotEmailMessage" size="0.875">Didn't you get the email? </rt-text><rt-button slot="resendEmailLink" size="0.875" type="cta-large" data-qa="resend-email-link">Resend email </rt-button><rt-link context="label" slot="troubleLoginLink" size="0.875" href="/reset-client" data-qa="reset-link">Having trouble logging in?</rt-link><rt-text class="terms-and-policies" slot="termsAndPolicies" size="0.75">By continuing, you agree to the <rt-link href="/policies/terms-and-policies" target="_blank" data-qa="terms-policies-link" style="--textColor: var(--blueLink);">Terms and Policies</rt-link>and the <rt-link href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" data-qa="privacy-policy-link" style="--textColor: var(--blueLink);">Privacy Policy</rt-link>and to receive email from <rt-link href="https://www.fandango.com/about-us" target="_blank" data-qa="about-fandango-link" style="--textColor: var(--blueLink);">Fandango Media Brands</rt-link>. </rt-text></login-check-email-screen></template><template slot="screens" id="login-error-screen"><login-error-screen data-qa="login-error"><rt-text slot="header" size="1.5" context="heading" data-qa="header">Something went wrong... </rt-text><rt-text slot="description1" size="1" context="label" data-qa="description1">Please try again. </rt-text><img slot="image" src="/assets/pizza-pie/images/icons/cognito-error.c55e509a7fd.svg" /><rt-text hidden slot="description2" size="1" context="label" data-qa="description2"></rt-text><rt-link slot="ctaLink" hidden context="label" size="0.875" data-qa="retry-link">Retry</rt-link></login-error-screen></template><template slot="screens" id="login-enter-password-screen"><login-enter-password-screen data-qa="login-enter-password-screen"><rt-text slot="title" size="1.5" style="--fontWeight: var(--franklinGothicMedium);">Welcome back! </rt-text><rt-text slot="username" data-qa="user-email">username@email.com </rt-text><input-label slot="inputPassword" state="default" data-qa="password-input-label"><label slot="label" for="pass">Password</label><input slot="input" id="pass" type="password" placeholder="Password" autocomplete="off" data-qa="password-input"></input></input-label><rt-button disabled slot="continueButton" type="cta-large" data-qa="continue-btn">Continue </rt-button><rt-button slot="emailLoginButton" theme="light" shape="pill" data-qa="send-email-btn">Send email to verify </rt-button><rt-link slot="forgotPasswordLink" theme="light" data-qa="forgot-password-link">Forgot password</rt-link><rt-text class="terms-and-policies" slot="termsAndPolicies" size="0.75">By continuing, you agree to the <rt-link href="/policies/terms-and-policies" target="_blank" data-qa="terms-policies-link" style="--textColor: var(--blueLink);">Terms and Policies</rt-link>and the <rt-link href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" data-qa="privacy-policy-link" style="--textColor: var(--blueLink);">Privacy Policy</rt-link>and to receive email from <rt-link href="https://www.fandango.com/about-us" target="_blank" data-qa="about-fandango-link" style="--textColor: var(--blueLink);">Fandango Media Brands</rt-link>. </rt-text></login-enter-password-screen></template><template slot="screens" id="login-start-screen"><login-start-screen data-qa="login-start-screen"><input-label slot="inputEmail" state="default" data-qa="email-input-label"><label slot="label" for="login-email-input">Email address</label><input slot="input" autocomplete="username" id="login-email-input" placeholder="Email address" type="text" data-qa="email-input" /></input-label><rt-button disabled slot="emailLoginButton" type="cta-large" data-qa="continue-btn">Continue </rt-button><rt-button slot="googleLoginButton" shape="pill" theme="light" style="--buttonHeight: 52px; --borderRadius: 32px;" data-qa="google-login-btn" data-type="google"><div class="social-login-btn-content"><img height="16px" width="16px" src="/assets/pizza-pie/images/vendor/google/google_logo.28d9eb28faa.svg" />Continue with Google </div></rt-button><rt-button slot="appleLoginButton" shape="pill" theme="light" style="--buttonHeight: 52px; --borderRadius: 32px;" data-qa="apple-login-btn" data-type="apple"><div class="social-login-btn-content"><rt-icon size="1" icon="apple"></rt-icon>Continue with apple </div></rt-button><rt-link slot="resetLink" class="reset-link" context="label" size="0.875" href="/reset-client" data-qa="reset-link">Having trouble logging in? </rt-link><rt-text class="terms-and-policies" slot="termsAndPolicies" size="0.75">By continuing, you agree to the <rt-link href="/policies/terms-and-policies" target="_blank" data-qa="terms-policies-link" style="--textColor: var(--blueLink);">Terms and Policies</rt-link>and the <rt-link href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" data-qa="privacy-policy-link" style="--textColor: var(--blueLink);">Privacy Policy</rt-link>and to receive email from <rt-link href="https://www.fandango.com/about-us" target="_blank" data-qa="about-fandango-link" style="--textColor: var(--blueLink);">Fandango Media Brands</rt-link>. </rt-text></login-start-screen></template><template slot="screens" id="login-success-screen"><login-success-screen data-qa="login-success-screen"><rt-text slot="status" size="1.5">Login successful! </rt-text><img slot="icon" src="/assets/pizza-pie/images/icons/cognito-success.3a3d5f32fab.svg" width="111" height="105" /></login-success-screen></template><template slot="screens" id="cognito-opt-in-us"><auth-optin-screen data-qa="auth-opt-in-screen"><div slot="newsletter-text"><h2 class="cognito-optin-form__header unset">Let's keep in touch!</h2></div><img slot="image" class="image" src="https://images.fandango.com/cms/assets/97c33f00-313f-11ee-9aaf-6762c75465cf--newsletter.png" alt="Rotten Tomatoes Newsletter"><h2 slot="sub-title" class="subTitle unset">Sign up for the Rotten Tomatoes newsletter to get weekly updates on: </h2><ul slot="options"><li class="icon-item">Upcoming Movies and TV shows</li><li class="icon-item">Rotten Tomatoes Podcast</li><li class="icon-item">Media News + More</li></ul><rt-button slot="opt-in-button" data-qa="auth-opt-in-screen-opt-in-btn">Sign me up </rt-button><rt-button slot="opt-out-button" class="button--outline" data-qa="auth-opt-in-screen-opt-out-btn">No thanks </rt-button><p slot="foot-note">By clicking "Sign Me Up," you are agreeing to receive occasional emails and communications from Fandango Media (Fandango, Vudu, and Rotten Tomatoes) and consenting to Fandango's <a href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" class="optin-link" target="_blank" rel="noopener" data-qa="auth-name-screen-privacy-policy-link">Privacy Policy</a>and <a href="/policies/terms-and-policies" class="optin-link" target="_blank" rel="noopener" data-qa="auth-name-screen-privacy-policy-link">Terms and Policies</a>. Please allow 10 business days for your account to reflect your preferences. </p></auth-optin-screen></template><template slot="screens" id="cognito-opt-in-foreign"><auth-optin-screen data-qa="auth-opt-in-screen"><div slot="newsletter-text"><h2 class="cognito-optin-form__header unset">Let's keep in touch!</h2></div><img slot="image" class="image" src="https://images.fandango.com/cms/assets/97c33f00-313f-11ee-9aaf-6762c75465cf--newsletter.png" alt="Rotten Tomatoes Newsletter"><h2 slot="sub-title" class="subTitle unset">Sign up for the Rotten Tomatoes newsletter to get weekly updates on: </h2><ul slot="options"><li class="icon-item">Upcoming Movies and TV shows</li><li class="icon-item">Rotten Tomatoes Podcast</li><li class="icon-item">Media News + More</li></ul><rt-button slot="opt-in-button" data-qa="auth-opt-in-screen-opt-in-btn">Sign me up </rt-button><rt-button slot="opt-out-button" class="button--outline" data-qa="auth-opt-in-screen-opt-out-btn">No thanks </rt-button></auth-optin-screen></template><template slot="screens" id="cognito-opt-in-success"><auth-verify-screen><rt-icon icon="check-circled" slot="icon"></rt-icon><p class="h3" slot="status">OK, got it!</p></auth-verify-screen></template></div><div id="emptyPlaceholder"></div><script ASYNC src="//assets.adobedtm.com/launch-EN549327edc13e414a9beb5d61bfd9aac6.min.js"></script><div id="main" class="container rt-layout__body"><a href="#main-page-content" class="skip-link">Skip to Main Content</a><div id="header_and_leaderboard"><div id="top_leaderboard_wrapper" class="leaderboard_wrapper "><ad-unit hidden unit-display="desktop" unit-type="topbanner" adjust-height><div slot="ad-inject"></div></ad-unit><ad-unit hidden unit-display="mobile" unit-type="mbanner"><div slot="ad-inject"></div></ad-unit></div></div><rt-header-manager></rt-header-manager><rt-header aria-label="navigation bar" class="navbar" data-qa="header-nav-bar" data-AdsGlobalNavTakeoverManager="header" id="header-main" skeleton="panel"><button aria-label="Open aRTi" class="arti-mobile" data-ArtiManager="btnArti:click" slot="arti-mobile"><img alt="arti" src="/assets/pizza-pie/images/arti.041d204c4a4.svg" /></button><div slot="mobile-header-nav"><rt-button id="mobile-header-nav-btn" data-RtHeaderManager="mobileHeaderNavBtn:click" size="1.6" style="--backgroundColor: transparent; --backgroundColorHover: transparent; --buttonPadding: 0 10px 4px;">&#9776; </rt-button><mobile-header-nav id="mobile-header-nav" data-RtHeaderManager="mobileHeaderNav"><rt-img slot="logoImage" alt="Rotten Tomatoes" fetchpriority="high" src="/assets/pizza-pie/images/rt-tomato-logo.20c3bdbc97b.svg"></rt-img><div slot="menusCss"></div><div slot="menus"></div></mobile-header-nav></div><a class="logo-wrap" data-AdsGlobalNavTakeoverManager="logoLink" data-SearchResultsNavManager="rtNavLogo" href="/" id="navbar" slot="logo"><img alt="Rotten Tomatoes" data-qa="header-logo" data-AdsGlobalNavTakeoverManager="logo" src="/assets/pizza-pie/images/rtlogo.9b892cff3fd.png" fetchpriority="high" /><div class="hide"><ad-unit hidden unit-display="desktop,mobile" unit-type="logorepeat" unit-targeting="ploc=rtlogo;"><div slot="ad-inject"></div></ad-unit></div></a><search-results-nav-manager></search-results-nav-manager><search-results-nav data-adobe-id="global-nav-search" data-SearchResultsNavManager="search" slot="search" skeleton="chip"><search-results-controls data-SearchResultsNavManager="searchControls" slot="controls"><input aria-label="Search" data-AdsGlobalNavTakeoverManager="searchInput" data-SearchResultsNavManager="inputText:click,input,keydown" data-qa="search-input" placeholder="Search" slot="search-input" type="text" /><rt-button class="search-clear" data-qa="search-clear" data-AdsGlobalNavTakeoverManager="searchClearBtn" data-SearchResultsNavManager="clearBtn:click" size="0.875" slot="search-clear" theme="transparent"><rt-icon icon="close"></rt-icon></rt-button><rt-link class="search-submit" aria-label="Submit search" data-qa="search-submit" data-AdsGlobalNavTakeoverManager="searchSubmitBtn" data-SearchResultsNavManager="submitBtn:click" href="/search" size="0.875" slot="search-submit"><rt-icon icon="search"></rt-icon></rt-link><rt-button class="search-cancel" data-qa="search-cancel" data-AdsGlobalNavTakeoverManager="searchCancelBtn" data-SearchResultsNavManager="cancelBtn:click" size="0.875" slot="search-cancel" theme="transparent">Cancel </rt-button></search-results-controls><search-results aria-expanded="false" class="hide" data-SearchResultsNavManager="searchResults" slot="results"></search-results></search-results-nav><ul slot="nav-links"><li><a href="/about" data-qa="header:link-whats-tmeter" data-AdsGlobalNavTakeoverManager="text">About Rotten Tomatoes&reg; </a></li><li><a href="/critics" data-qa="header:link-critics-home" data-AdsGlobalNavTakeoverManager="text">Critics </a></li><li data-RtHeaderManager="loginLink"><ul><li><button id="masthead-show-login-btn" class="js-cognito-signin button--link" data-AuthInitiateManager="btnSignIn:click" data-qa="header:login-btn" data-AdsGlobalNavTakeoverManager="text">Login/signup </button></li></ul></li><li class="hide" data-RtHeaderManager="userItem:keydown,keyup,mouseenter" data-qa="header:user"><a class="masthead-user-link" data-RtHeaderManager="navUserlink:focus" rel="nofollow" data-qa="user-profile-link"><img data-RtHeaderManager="navUserImg" data-qa="user-profile-thumb"><p data-AdsGlobalNavTakeoverManager="text" data-RtHeaderManager="navUserFirstName" data-qa="user-profile-name"></p><rt-icon data-AdsGlobalNavTakeoverManager="text" icon="down-dir" image></rt-icon></a><rt-header-user-info class="hide" data-RtHeaderManager="userInfo:focusout,mouseleave"><a data-qa="user-stats-profile-pic" href="" rel="nofollow" slot="imageExpanded" tabindex="-1"><img src="" width="40" alt=""></a><a slot="fullName" rel="nofollow" href="" class="username" data-qa="user-stats-name"></a><a slot="wts" rel="nofollow" href="" class="wts-count-block" data-qa="user-stats-wts"><rt-icon icon="plus" data-qa="user-stats-ratings-count"></rt-icon><span class="count" data-qa="user-stats-wts-count"></span>&nbsp;Wants to See </a><a slot="rating" rel="nofollow" href="" class="rating-count-block" data-qa="user-stats-ratings"><rt-icon icon="star" data-qa="user-stats-ratings-count"></rt-icon><span class="count"></span>&nbsp;Ratings </a><a slot="profileLink" rel="nofollow" class="dropdown-link" href="" data-qa="user-stats-profile-link">Profile</a><a slot="accountLink" rel="nofollow" class="dropdown-link" href="/user/account" data-qa="user-stats-account-link">Account</a><a slot="logoutLink" class="dropdown-link" data-RtHeaderManager="logoutLink:click" href="#logout" data-qa="user-stats-logout-link">Log Out</a></rt-header-user-info></li></ul><rt-header-nav slot="nav-dropdowns"><button aria-label="Open aRTi" class="arti-desktop" data-ArtiManager="btnArti:click" slot="arti-desktop"><img alt="arti" src="/assets/pizza-pie/images/arti.041d204c4a4.svg" /></button><rt-header-nav-item slot="movies" data-qa="masthead:movies-dvds"><a class="unset" slot="link" href="/browse/movies_in_theaters/sort:popular" data-qa="masthead:movies-dvds-link" data-AdsGlobalNavTakeoverManager="text">Movies </a><rt-header-nav-item-dropdown aria-expanded="false" slot="dropdown" data-qa="movies-menu"><rt-header-nav-item-dropdown-list slot="column" data-qa="header-movies-in-theaters"><p slot="title" class="h4" data-qa="movies-in-theaters-main-link"><a class="unset" href="/browse/movies_in_theaters/sort:popular">Movies in theaters</a></p><ul slot="links"><li data-qa="in-theaters-item"><a href="/browse/movies_in_theaters/sort:newest" data-qa="opening-this-week-link">Opening This Week</a></li><li data-qa="in-theaters-item"><a href="/browse/movies_in_theaters/sort:top_box_office" data-qa="top-box-office-link">Top Box Office</a></li><li data-qa="in-theaters-item"><a href="/browse/movies_coming_soon/" data-qa="coming-soon-link">Coming Soon to Theaters</a></li><li data-qa="in-theaters-item"><a href="/browse/movies_in_theaters/critics:certified_fresh~sort:popular" data-qa="certified-fresh-link">Certified Fresh Movies</a></li></ul></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-on-dvd-streaming"><p slot="title" class="h4" data-qa="dvd-streaming-main-link"><a class="unset" href="/browse/movies_at_home">Movies at Home</a></p><ul slot="links"><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/affiliates:fandango-at-home" data-qa="fandango-at-home-link">Fandango at Home</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/affiliates:peacock" data-qa="peacock-link">Peacock</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/affiliates:netflix" data-qa="netflix-link">Netflix</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/affiliates:apple-tv-plus" data-qa="apple-tv-link">Apple TV+</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/affiliates:prime-video" data-qa="prime-video-link">Prime Video</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/sort:popular" data-qa="most-popular-streaming-movies-link">Most Popular Streaming movies</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/critics:certified_fresh" data-qa="certified-fresh-movies-link">Certified Fresh movies</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home" data-qa="browse-all-link">Browse all</a></li></ul></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-movies-more"><p slot="title" class="h4">More</p><ul slot="links"><li data-qa="what-to-watch-item"><a href="https://editorial.rottentomatoes.com/rt-hub/what-to-watch" class="what-to-watch" data-qa="what-to-watch-link">What to Watch<rt-badge>New</rt-badge></a></li></ul></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" cfp><p slot="title" class="h4">Certified fresh picks</p><ul slot="links" class="cfp-wrap" data-qa="header-certified-fresh-picks" data-curation="rt-nav-list-cf-picks"><li data-qa="cert-fresh-item"><a class="cfp-tile" href="/m/twinless" data-qa="cert-fresh-link"><tile-dynamic data-qa="tile"><rt-img alt="Twinless poster image" slot="image" src="https://resizing.flixster.com/j7lw2KeY9_XyfZQdqRZGku7_9C8=/206x305/v2/https://resizing.flixster.com/uxoeWz7uWmeYIV94_SzEV_osqe4=/fit-in/180x240/v2/https://resizing.flixster.com/VlylB3xT2RIYmRivMx37O3yD76Q=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2ZlNDQ1MGQ5LTFjN2QtNDIwNC04NWE1LTM5NGM4N2U5ZTgzYy5qcGc=" loading="lazy"></rt-img><div slot="caption" data-track="scores"><div class="score-wrap"><score-icon-critics certified sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" size="1" context="label">98%</rt-text></div><span class="p--small">Twinless</span><span class="sr-only">Link to Twinless</span></div></tile-dynamic></a></li><li data-qa="cert-fresh-item"><a class="cfp-tile" href="/m/hamilton_2020" data-qa="cert-fresh-link"><tile-dynamic data-qa="tile"><rt-img alt="Hamilton poster image" slot="image" src="https://resizing.flixster.com/1woquJmQfEhWCZtm7GcH0NMHsYA=/206x305/v2/https://resizing.flixster.com/PeAJ5ZpF5qB98ZiX6ixNDCgW2P0=/fit-in/180x240/v2/https://resizing.flixster.com/VmBvlTk8-z7pQvDZXTgSdj93WDE=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzkzY2IxZjFkLTE1NjEtNDQ4Yi05NDY3LTcxNzFmMDVhMDczNi5qcGc=" loading="lazy"></rt-img><div slot="caption" data-track="scores"><div class="score-wrap"><score-icon-critics certified sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" size="1" context="label">98%</rt-text></div><span class="p--small">Hamilton</span><span class="sr-only">Link to Hamilton</span></div></tile-dynamic></a></li><li data-qa="cert-fresh-item"><a class="cfp-tile" href="/m/the_thursday_murder_club" data-qa="cert-fresh-link"><tile-dynamic data-qa="tile"><rt-img alt="The Thursday Murder Club poster image" slot="image" src="https://resizing.flixster.com/jeeldFGcfSMgG09ey5VB7TCFiek=/206x305/v2/https://resizing.flixster.com/9LXDkCzIBBNEiPURkB9t6VefF5Q=/fit-in/180x240/v2/https://resizing.flixster.com/rwdeR5xIiN0k7SWr6yXdnmb6zP8=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2EzYWFkZWJiLWE5N2MtNDc3MS1iMDRlLTk0YWVlYzI5M2UxZS5qcGc=" loading="lazy"></rt-img><div slot="caption" data-track="scores"><div class="score-wrap"><score-icon-critics certified sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" size="1" context="label">76%</rt-text></div><span class="p--small">The Thursday Murder Club</span><span class="sr-only">Link to The Thursday Murder Club</span></div></tile-dynamic></a></li></ul></rt-header-nav-item-dropdown-list></rt-header-nav-item-dropdown></rt-header-nav-item><rt-header-nav-item slot="tv" data-qa="masthead:tv"><a class="unset" slot="link" href="/browse/tv_series_browse/sort:popular" data-qa="masthead:tv-link" data-AdsGlobalNavTakeoverManager="text">Tv shows </a><rt-header-nav-item-dropdown aria-expanded="false" slot="dropdown" data-qa="tv-menu"><rt-header-nav-item-dropdown-list slot="column" data-qa="header-tv-list1"><p slot="title" class="h4" data-curation="rt-hp-text-list-3">New TV Tonight </p><ul slot="links" class="score-list-wrap"><li data-qa="list-item"><a class="score-list-item" href="/tv/task/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">89%</rt-text></div><span>Task: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/the_walking_dead_daryl_dixon/s03" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">80%</rt-text></div><span>The Walking Dead: Daryl Dixon: Season 3 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/the_crow_girl/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">80%</rt-text></div><span>The Crow Girl: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/only_murders_in_the_building/s05" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>Only Murders in the Building: Season 5 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/the_girlfriend/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>The Girlfriend: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/aka_charlie_sheen/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>aka Charlie Sheen: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/wizards_beyond_waverly_place/s02" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>Wizards Beyond Waverly Place: Season 2 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/seen_and_heard_the_history_of_black_television/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>Seen &amp; Heard: the History of Black Television: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/the_fragrant_flower_blooms_with_dignity/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>The Fragrant Flower Blooms With Dignity: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/guts_and_glory/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>Guts &amp; Glory: Season 1 </span></a></li></ul><a class="a--short" data-qa="tv-list1-view-all-link" href="/browse/tv_series_browse/sort:newest" slot="view-all-link">View All </a></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-tv-list2"><p slot="title" class="h4" data-curation="rt-hp-text-list-2">Most Popular TV on RT </p><ul slot="links" class="score-list-wrap"><li data-qa="list-item"><a class="score-list-item" href="/tv/the_paper_2025/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">83%</rt-text></div><span>The Paper: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/dexter_resurrection/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">95%</rt-text></div><span>Dexter: Resurrection: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/alien_earth/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">95%</rt-text></div><span>Alien: Earth: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/task/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">89%</rt-text></div><span>Task: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/wednesday/s02" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">87%</rt-text></div><span>Wednesday: Season 2 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/peacemaker_2022/s02" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">99%</rt-text></div><span>Peacemaker: Season 2 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/the_terminal_list_dark_wolf/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">73%</rt-text></div><span>The Terminal List: Dark Wolf: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/hostage_2025/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">82%</rt-text></div><span>Hostage: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/chief_of_war/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">93%</rt-text></div><span>Chief of War: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/irish_blood/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">100%</rt-text></div><span>Irish Blood: Season 1 </span></a></li></ul><a class="a--short" data-qa="tv-list2-view-all-link" href="/browse/tv_series_browse/sort:popular?" slot="view-all-link">View All </a></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-tv-more"><p slot="title" class="h4">More</p><ul slot="links"><li><a href="https://editorial.rottentomatoes.com/rt-hub/what-to-watch/" class="what-to-watch" data-qa="what-to-watch-link-tv">What to Watch<rt-badge>New</rt-badge></a></li><li><a href="/browse/tv_series_browse/sort:popular" data-qa="tv-best-link"><span>Best TV Shows</span></a></li><li><a href="/browse/tv_series_browse/sort:popular" data-qa="tv-popular-link"><span>Most Popular TV</span></a></li><li><a href="/browse/tv_series_browse/affiliates:fandango-at-home" data-qa="tv-fandango-at-home-link"><span>Fandango at Home</span></a></li><li><a href="/browse/tv_series_browse/affiliates:peacock" data-qa="tv-peacock-link"><span>Peacock</span></a></li><li><a href="/browse/tv_series_browse/affiliates:paramount-plus" data-qa="tv-paramount-link"><span>Paramount+</span></a></li><li><a href="/browse/tv_series_browse/affiliates:netflix" data-qa="tv-netflix-link"><span>Netflix</span></a></li><li><a href="/browse/tv_series_browse/affiliates:prime-video" data-qa="tv-prime-video-link"><span>Prime Video</span></a></li><li><a href="/browse/tv_series_browse/affiliates:apple-tv-plus" data-qa="tv-apple-tv-plus-link"><span>Apple TV+</span></a></li></ul></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" cfp data-qa="header-certified-fresh-pick"><p slot="title" class="h4">Certified fresh pick </p><ul slot="links" class="cfp-wrap" data-curation="rt-nav-list-cf-picks"><li><a class="cfp-tile" href="/tv/the_paper_2025/s01" data-qa="cert-fresh-link"><tile-dynamic data-qa="tile"><rt-img alt="The Paper: Season 1 poster image" slot="image" src="https://resizing.flixster.com/yFijQcjPYUWUelgmiZLHgkXU7hw=/206x305/v2/https://resizing.flixster.com/DFkkHf5pEVX_apKtIQZcoEvI6RU=/fit-in/180x240/v2/https://resizing.flixster.com/texEZJLAG-KcVpfCdkT2R1t4cmE=/ems.cHJkLWVtcy1hc3NldHMvdHZzZWFzb24vYTM3OWM2MTctN2M3Ny00MjdhLTk4NDUtODE5ZWUwMWExNGRhLnBuZw==" loading="lazy"></rt-img><div slot="caption" data-track="scores"><div class="score-wrap"><score-icon-critics certified sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" size="1" context="label">83%</rt-text></div><span class="p--small">The Paper: Season 1</span><span class="sr-only">Link to The Paper: Season 1</span></div></tile-dynamic></a></li></ul></rt-header-nav-item-dropdown-list></rt-header-nav-item-dropdown></rt-header-nav-item><rt-header-nav-item slot="shop"><a class="unset" id="appLink" slot="link" href="https://editorial.rottentomatoes.com/article/app/" target="_blank" data-qa="masthead:app-link" data-AdsGlobalNavTakeoverManager="text">RT App <temporary-display slot="temporary-display" key="app" element="#appLink" event="click"><rt-badge hidden>New</rt-badge></temporary-display></a></rt-header-nav-item><rt-header-nav-item slot="news" data-qa="masthead:news"><a class="unset" slot="link" href="https://editorial.rottentomatoes.com/" data-qa="masthead:news-link" data-AdsGlobalNavTakeoverManager="text">News </a><rt-header-nav-item-dropdown aria-expanded="false" slot="dropdown" data-qa="news-menu"><rt-header-nav-item-dropdown-list slot="column" data-qa="header-news-columns"><p slot="title" class="h4">Columns</p><ul slot="links"><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/all-time-lists/" data-pageheader="All-Time Lists" data-qa="column-link">All-Time Lists </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/binge-guide/" data-pageheader="Binge Guide" data-qa="column-link">Binge Guide </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/comics-on-tv/" data-pageheader="Comics on TV" data-qa="column-link">Comics on TV </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/countdown/" data-pageheader="Countdown" data-qa="column-link">Countdown </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/five-favorite-films/" data-pageheader="Five Favorite Films" data-qa="column-link">Five Favorite Films </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/video-interviews/" data-pageheader="Video Interviews" data-qa="column-link">Video Interviews </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/weekend-box-office/" data-pageheader="Weekend Box Office" data-qa="column-link">Weekend Box Office </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/weekly-ketchup/" data-pageheader="Weekly Ketchup" data-qa="column-link">Weekly Ketchup </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/what-to-watch/" data-pageheader="What to Watch" data-qa="column-link">What to Watch </a></li></ul></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-news-guides"><p slot="title" class="h4">Guides</p><ul slot="links" class="news-wrap"><li data-qa="guides-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/guide/best-football-movies/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="59 Best Football Movies, Ranked by Tomatometer poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2023/09/600EssentialFootballMovies.png" loading="lazy"></rt-img><div slot="caption"><p>59 Best Football Movies, Ranked by Tomatometer</p><span class="sr-only">Link to 59 Best Football Movies, Ranked by Tomatometer</span></div></tile-dynamic></a></li><li data-qa="guides-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/guide/best-new-rom-coms-romance-movies/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="50 Best New Rom-Coms and Romance Movies poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2025/08/Best_New_Romcoms600.jpg" loading="lazy"></rt-img><div slot="caption"><p>50 Best New Rom-Coms and Romance Movies</p><span class="sr-only">Link to 50 Best New Rom-Coms and Romance Movies</span></div></tile-dynamic></a></li></ul><a class="a--short" data-qa="guides-view-all-link" href="https://editorial.rottentomatoes.com/countdown/" slot="view-all-link">View All </a></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-news-hubs"><p slot="title" class="h4">Hubs</p><ul slot="links" class="news-wrap"><li data-qa="hubs-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/rt-hub/what-to-watch/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="What to Watch: In Theaters and On Streaming poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2023/05/RT_WTW_Generic_2023_Thumbnail_600x314_021623.jpg" loading="lazy"></rt-img><div slot="caption"><p>What to Watch: In Theaters and On Streaming</p><span class="sr-only">Link to What to Watch: In Theaters and On Streaming</span></div></tile-dynamic></a></li><li data-qa="hubs-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/rt-hub/awards-tour/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="Awards Tour poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2023/02/RT_AwardsTour_Thumbnail_600x314.jpg" loading="lazy"></rt-img><div slot="caption"><p>Awards Tour</p><span class="sr-only">Link to Awards Tour</span></div></tile-dynamic></a></li></ul><a class="a--short" data-qa="hubs-view-all-link" href="https://editorial.rottentomatoes.com/rt-hubs/" slot="view-all-link">View All </a></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-news-rt-news"><p slot="title" class="h4">RT News</p><ul slot="links" class="news-wrap"><li data-qa="rt-news-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/article/new-movies-and-shows-streaming-in-september-2025-what-to-watch-on-netflix-prime-video-hbo-max-disney-and-more/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="New Movies and Shows Streaming in September: What to watch on Netflix, Prime Video, HBO Max, Disney+ and More poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2025/08/New_Streaming_September_2025-Rep.jpg" loading="lazy"></rt-img><div slot="caption"><p>New Movies and Shows Streaming in September: What to watch on Netflix, Prime Video, HBO Max, Disney+ and More</p><span class="sr-only">Link to New Movies and Shows Streaming in September: What to watch on Netflix, Prime Video, HBO Max, Disney+ and More</span></div></tile-dynamic></a></li><li data-qa="rt-news-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/article/the-conjuring-last-rites-first-reviews/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="<em>The Conjuring: Last Rites</em>First Reviews: A Frightful, Fitting Send-off poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2025/09/Conjuring_Last_Rites_Reviews-Rep.jpg" loading="lazy"></rt-img><div slot="caption"><p><em>The Conjuring: Last Rites</em>First Reviews: A Frightful, Fitting Send-off</p><span class="sr-only">Link to <em>The Conjuring: Last Rites</em>First Reviews: A Frightful, Fitting Send-off</span></div></tile-dynamic></a></li></ul><a class="a--short" data-qa="rt-news-view-all-link" href="https://editorial.rottentomatoes.com/news/" slot="view-all-link">View All </a></rt-header-nav-item-dropdown-list></rt-header-nav-item-dropdown></rt-header-nav-item><rt-header-nav-item slot="showtimes"><a class="unset" slot="link" href="https://www.fandango.com/movies-in-theaters?a=13036" target="_blank" rel="noopener" data-qa="masthead:tickets-showtimes-link" data-AdsGlobalNavTakeoverManager="text">Showtimes </a></rt-header-nav-item></rt-header-nav></rt-header><ads-global-nav-takeover-manager></ads-global-nav-takeover-manager><section class="trending-bar"><ad-unit hidden id="trending_bar_ad" unit-display="desktop" unit-type="trendinggraphic"><div slot="ad-inject"></div></ad-unit><div id="trending-bar-start" class="trending-list-wrap" data-qa="trending-bar"><ul class="list-inline trending-bar__list" data-curation="rt-nav-trending" data-qa="trending-bar-list"><li class="trending-bar__header">Trending on RT</li><li><a class="trending-bar__link" href="https://editorial.rottentomatoes.com/article/2025-emmys-ballot-complete-with-tomatometer-and-popcornmeter-scores/" data-qa="trending-bar-item">Emmy Noms </a></li><li><a class="trending-bar__link" href="https://editorial.rottentomatoes.com/article/movie-re-releases-calendar/" data-qa="trending-bar-item">Re-Release Calendar </a></li><li><a class="trending-bar__link" href="https://editorial.rottentomatoes.com/article/renewed-and-cancelled-tv-shows-2025/" data-qa="trending-bar-item">Renewed and Cancelled TV </a></li><li><a class="trending-bar__link" href="https://editorial.rottentomatoes.com/article/app/" data-qa="trending-bar-item">The Rotten Tomatoes App </a></li></ul><div class="trending-bar__social" data-qa="trending-bar-social-list"><social-media-icons theme="light" size="14"></social-media-icons></div></div></section><main id="main_container" class="container rt-layout__content"><div id="main-page-content"><div id="movie-overview" data-HeroModulesManager="overviewWrap"><watchlist-button-manager></watchlist-button-manager><div id="hero-wrap" data-AdUnitManager="heroWrap" data-AdsMediaScorecardManager="heroWrap" data-HeroModulesManager="heroWrap"><div aria-labelledby="media-hero-label" class="media-hero-wrap" skeleton="panel" data-adobe-id="media-hero" data-qa="section:media-hero" data-HeroModulesManager="mediaHeroWrap"><h1 class="unset" id="media-hero-label"><sr-text>The Fantastic Four: First Steps </sr-text></h1><media-hero averagecolor="30,10,15" mediatype="Movie" scrolly="0" scrollystart="0" data-AdsMediaScorecardManager="mediaHero" data-HeroModulesManager="mediaHero:collapse"><rt-button slot="iconicVideoCta" theme="transparent" data-content-type="PROMO" data-ems-id="db64beee-683b-39ce-9617-94f6b67aa997" data-mpx-id="2441310275805" data-public-id="BBgYzBvAIQDN" data-title="The Fantastic Four: First Steps" data-type="Movie" data-VideoPlayerOverlayManager="btnVideo:click"><sr-text>Play trailer</sr-text></rt-button><rt-text slot="iconicVideoRuntime" size="0.75">0:59</rt-text><rt-img slot="iconic" alt="Main image for The Fantastic Four: First Steps" fallbacktheme="iconic" fetchpriority="high" src="https://resizing.flixster.com/buY_bX63TK2bMR31-aeM1FU9NUI=/375x210/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/663/931/thumb_4ca6d6c5-6a62-11f0-94b5-022bbbb30d69.jpg,https://resizing.flixster.com/9mw5ksRy6QUjRmIL1o2jk7tcV2o=/620x336/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/663/931/thumb_4ca6d6c5-6a62-11f0-94b5-022bbbb30d69.jpg"></rt-img><img slot="poster" alt="Poster for " fetchpriority="high" src="https://resizing.flixster.com/-im_GEu4fu_y_2eZvk0DmD-JWQI=/68x102/v2/https://resizing.flixster.com/WEfe-vTCqjHioD77J7f-qeoZZdY=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2FjN2M2NzA0LWNiMTctNDhjMy05N2VlLTk3YWI3N2JhZDZmMS5qcGc=" /><rt-text slot="title" size="1.25,1.75" context="heading">The Fantastic Four: First Steps</rt-text><rt-text slot="episodeTitle" size="1,1.5" context="label"></rt-text><rt-text slot="metadataProp" context="label" size="0.875">PG-13</rt-text><rt-text slot="metadataProp" context="label" size="0.875">Now Playing</rt-text><rt-text slot="metadataProp" context="label" size="0.875">1h 54m</rt-text><rt-text slot="metadataGenre" size="0.875">Action</rt-text><rt-text slot="metadataGenre" size="0.875">Adventure</rt-text><rt-text slot="metadataGenre" size="0.875">Sci-Fi</rt-text><rt-text slot="metadataGenre" size="0.875">Fantasy</rt-text><rt-button slot="trailerCta" shape="pill" theme="light" data-content-type="PROMO" data-ems-id="db64beee-683b-39ce-9617-94f6b67aa997" data-mpx-id="2441310275805" data-public-id="BBgYzBvAIQDN" data-title="The Fantastic Four: First Steps" data-type="Movie" data-VideoPlayerOverlayManager="btnVideo:click"><rt-icon icon="play"></rt-icon><sr-text>Play</sr-text>Trailer </rt-button><watchlist-button slot="watchlistCta" emsid="db64beee-683b-39ce-9617-94f6b67aa997" mediatype="Movie" mediatitle="The Fantastic Four: First Steps" state="unchecked" theme="transparent-lighttext" data-HeroModulesManager="mediaHeroWatchlistBtn" data-WatchlistButtonManager="watchlistButton:click"><span slot="text">Watchlist</span></watchlist-button><watchlist-button slot="mobileWatchlistCta" emsid="db64beee-683b-39ce-9617-94f6b67aa997" mediatype="Movie" mediatitle="The Fantastic Four: First Steps" state="unchecked" data-HeroModulesManager="mediaHeroWatchlistBtn" data-WatchlistButtonManager="watchlistButton:click"></watchlist-button><div slot="desktopVideos" data-HeroModulesManager="mediaHeroVideos"></div><rt-button slot="collapsedPrimaryCta" hidden shape="pill" theme="simplified" data-AdsMediaScorecardManager="collapsedPrimaryCta" data-HeroModulesManager="mediaHeroCta:click"></rt-button><watchlist-button slot="collapsedWatchlistCta" emsid="db64beee-683b-39ce-9617-94f6b67aa997" mediatype="Movie" mediatitle="The Fantastic Four: First Steps" state="unchecked" theme="transparent-lighttext" data-HeroModulesManager="mediaHeroWatchlistBtn" data-WatchlistButtonManager="watchlistButton:click"><span slot="text">Watchlist</span></watchlist-button><score-icon-critics slot="collapsedCriticsIcon" size="2.5"></score-icon-critics><rt-text slot="collapsedCriticsScore" context="label" size="1.375"></rt-text><rt-link slot="collapsedCriticsLink" size="0.75"></rt-link><rt-text slot="collapsedCriticsLabel" size="0.75">Tomatometer</rt-text><score-icon-audience slot="collapsedAudienceIcon" size="2.5"></score-icon-audience><rt-text slot="collapsedAudienceScore" context="label" size="1.375"></rt-text><rt-link slot="collapsedAudienceLink" size="0.75"></rt-link><rt-text slot="collapsedAudienceLabel" size="0.75">Popcornmeter</rt-text></media-hero><script id="media-hero-json" data-json="mediaHero" type="application/json">{"averageColorHsl":"30,10,15","iconic":{"srcDesktop":"https://resizing.flixster.com/9mw5ksRy6QUjRmIL1o2jk7tcV2o=/620x336/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/663/931/thumb_4ca6d6c5-6a62-11f0-94b5-022bbbb30d69.jpg","srcMobile":"https://resizing.flixster.com/buY_bX63TK2bMR31-aeM1FU9NUI=/375x210/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/663/931/thumb_4ca6d6c5-6a62-11f0-94b5-022bbbb30d69.jpg"},"content":{"episodeTitle":"","metadataGenres":["Action","Adventure","Sci-Fi","Fantasy"],"metadataProps":["PG-13","Now Playing","1h 54m"],"posterSrc":"https://resizing.flixster.com/-im_GEu4fu_y_2eZvk0DmD-JWQI=/68x102/v2/https://resizing.flixster.com/WEfe-vTCqjHioD77J7f-qeoZZdY=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2FjN2M2NzA0LWNiMTctNDhjMy05N2VlLTk3YWI3N2JhZDZmMS5qcGc=","title":"The Fantastic Four: First Steps","primaryVideo":{"contentType":"PROMO","durationInSeconds":"59.977","mpxId":"2441310275805","publicId":"BBgYzBvAIQDN","thumbnail":{"url":"https://statcdn.fandango.com/MPX/image/NBCU_Fandango/663/931/thumb_4ca6d6c5-6a62-11f0-94b5-022bbbb30d69.jpg"},"title":"The Fantastic Four: First Steps: 4 Us All","runtime":"0:59"}}} </script></div><hero-modules-manager><script data-json="vanity" type="application/json">{"emsId":"db64beee-683b-39ce-9617-94f6b67aa997","href":"/m/the_fantastic_four_first_steps","lifecycleWindow":{"date":"2025-07-25","lifecycle":"IN_THEATERS"},"title":"The Fantastic Four: First Steps","type":"movie","value":"the_fantastic_four_first_steps","parents":[],"mediaType":"Movie"}</script></hero-modules-manager></div><div id="main-wrap"><div id="modules-wrap" data-curation="drawer"><div class="media-scorecard no-border" data-adobe-id="media-scorecard" data-qa="section:media-scorecard"><media-scorecard hideaudiencescore="false" skeleton="panel" data-AdsMediaScorecardManager="mediaScorecard" data-HeroModulesManager="mediaScorecard"><rt-img alt="poster image" loading="lazy" slot="posterImage" src="https://resizing.flixster.com/GRRDF-MY6_iS5Em5vNBg-Jd-uL0=/206x305/v2/https://resizing.flixster.com/WEfe-vTCqjHioD77J7f-qeoZZdY=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2FjN2M2NzA0LWNiMTctNDhjMy05N2VlLTk3YWI3N2JhZDZmMS5qcGc="></rt-img><rt-button slot="criticsScoreIcon" data-MediaScorecardManager="overlayOpen:click" theme="transparent"><score-icon-critics certified="true" sentiment="POSITIVE" size="2.5"></score-icon-critics></rt-button><rt-text slot="criticsScore" context="label" role="button" size="1.375" data-MediaScorecardManager="overlayOpen:click">87%</rt-text><rt-text slot="criticsScoreType" class="critics-score-type" role="button" size="0.75" data-MediaScorecardManager="overlayOpen:click">Tomatometer</rt-text><rt-link slot="criticsReviews" size="0.75" href="/m/the_fantastic_four_first_steps/reviews">390 Reviews </rt-link><rt-button slot="audienceScoreIcon" data-MediaScorecardManager="overlayOpen:click" theme="transparent"><score-icon-audience certified="true" size="2.5" sentiment="POSITIVE"></score-icon-audience></rt-button><rt-text slot="audienceScore" context="label" role="button" size="1.375" data-MediaScorecardManager="overlayOpen:click">91%</rt-text><rt-text slot="audienceScoreType" class="audience-score-type" role="button" size="0.75" data-MediaScorecardManager="overlayOpen:click">Popcornmeter</rt-text><rt-link slot="audienceReviews" size="0.75" href="/m/the_fantastic_four_first_steps/reviews?type=user">10,000+ Verified Ratings </rt-link><div slot="description" data-AdsMediaScorecardManager="description"><drawer-more maxlines="2" skeleton="panel" status="closed" style="--display: flex; gap: 4px;"><rt-text slot="content" size="1">Set against the vibrant backdrop of a 1960s-inspired, retro-futuristic world, Marvel Studios&#39; &quot;The Fantastic Four: First Steps&quot; introduces Marvel&#39;s First Family--Reed Richards/Mister Fantastic (Pedro Pascal), Sue Storm/Invisible Woman (Vanessa Kirby), Johnny Storm/Human Torch (Joseph Quinn) and Ben Grimm/The Thing (Ebon Moss-Bachrach) as they face their most daunting challenge yet. Forced to balance their roles as heroes with the strength of their family bond, they must defend Earth from a ravenous space god called Galactus (Ralph Ineson) and his enigmatic Herald, Silver Surfer (Julia Garner). And if Galactus&#39; plan to devour the entire planet and everyone on it weren&#39;t bad enough, it suddenly gets very personal. </rt-text><rt-link slot="ctaOpen"><rt-icon icon="down-open"></rt-icon></rt-link><rt-link slot="ctaClose"><rt-icon icon="up-open"></rt-icon></rt-link></drawer-more></div><affiliate-icon data-AdsMediaScorecardManager="affiliateIcon" icon="fandango" slot="affiliateIcon"></affiliate-icon><rt-img data-AdsMediaScorecardManager="affiliateIconCustom" slot="affiliateIconCustom" hidden></rt-img><rt-text context="label" data-AdsMediaScorecardManager="affiliatePrimaryText" size="1" slot="affiliatePrimaryText">Now in Theaters</rt-text><rt-text data-AdsMediaScorecardManager="affiliateSecondaryText" size="0.75" slot="affiliateSecondaryText">Now Playing</rt-text><rt-button arialabel="Buy tickets for The Fantastic Four: First Steps" href="https://www.fandango.com/the-fantastic-four-first-steps-2025-236967/movie-overview?cmp=rt_leaderboard&amp;a=13036" rel="noopener" shape="pill" slot="affiliateCtaBtn" style="--backgroundColor: #3478C1; --textColor: #FFFFFF;" target="_blank" theme="simplified" data-AdsMediaScorecardManager="affiliateCtaBtn" data-HeroModulesManager="mediaScorecardCta:click">Buy Tickets </rt-button><div slot="adImpressions"></div></media-scorecard><media-scorecard-manager><script id="media-scorecard-json" data-json="mediaScorecard" type="application/json">{"audienceScore":{"certifiedFresh":"certified","averageRating":"4.4","bandedRatingCount":"10,000+ Verified Ratings","likedCount":19671,"notLikedCount":1994,"reviewCount":5804,"score":"91","scoreType":"VERIFIED","sentiment":"POSITIVE","certified":true,"reviewsPageUrl":"/m/the_fantastic_four_first_steps/reviews?type=user","scorePercent":"91%","title":"Popcornmeter"},"criticsScore":{"averageRating":"7.20","certified":true,"likedCount":338,"notLikedCount":52,"ratingCount":390,"reviewCount":390,"score":"87","sentiment":"POSITIVE","reviewsPageUrl":"/m/the_fantastic_four_first_steps/reviews","scorePercent":"87%","title":"Tomatometer"},"criticReviewHref":"/critics/self-submission/movie/db64beee-683b-39ce-9617-94f6b67aa997","cta":{"buttonText":"Buy Tickets","buttonAnnouncement":"Buy tickets for The Fantastic Four: First Steps","windowText":"Now in Theaters","affiliate":"fandango","buttonStyle":{"backgroundColor":"#3478C1","textColor":"#FFFFFF"},"buttonUrl":"https://www.fandango.com/the-fantastic-four-first-steps-2025-236967/movie-overview?cmp=rt_leaderboard&a=13036","icon":"fandango","windowDate":"Now Playing"},"description":"Set against the vibrant backdrop of a 1960s-inspired, retro-futuristic world, Marvel Studios' \"The Fantastic Four: First Steps\" introduces Marvel's First Family--Reed Richards/Mister Fantastic (Pedro Pascal), Sue Storm/Invisible Woman (Vanessa Kirby), Johnny Storm/Human Torch (Joseph Quinn) and Ben Grimm/The Thing (Ebon Moss-Bachrach) as they face their most daunting challenge yet. Forced to balance their roles as heroes with the strength of their family bond, they must defend Earth from a ravenous space god called Galactus (Ralph Ineson) and his enigmatic Herald, Silver Surfer (Julia Garner). And if Galactus' plan to devour the entire planet and everyone on it weren't bad enough, it suddenly gets very personal.","hideAudienceScore":false,"overlay":{"audienceAll":{"certifiedFresh":"certified","averageRating":"4.3","bandedRatingCount":"25,000+ Ratings","likedCount":32334,"notLikedCount":4839,"reviewCount":11639,"score":"87","scoreType":"ALL","sentiment":"POSITIVE","certified":true,"reviewsPageUrl":"/m/the_fantastic_four_first_steps/reviews?type=user","scorePercent":"87%","title":"Popcornmeter","scoreLinkUrl":"/m/the_fantastic_four_first_steps/reviews?type=user"},"audienceTitle":"Popcornmeter","audienceVerified":{"certifiedFresh":"certified","averageRating":"4.4","bandedRatingCount":"10,000+ Verified Ratings","likedCount":19671,"notLikedCount":1994,"reviewCount":5804,"score":"91","scoreType":"VERIFIED","sentiment":"POSITIVE","certified":true,"reviewsPageUrl":"/m/the_fantastic_four_first_steps/reviews?type=user","scorePercent":"91%","title":"Popcornmeter","scoreLinkUrl":"/m/the_fantastic_four_first_steps/reviews?type=verified_audience"},"criticsAll":{"averageRating":"7.20","certified":true,"likedCount":338,"notLikedCount":52,"ratingCount":390,"reviewCount":390,"score":"87","sentiment":"POSITIVE","reviewsPageUrl":"/m/the_fantastic_four_first_steps/reviews","scorePercent":"87%","title":"Tomatometer","scoreLinkUrl":"/m/the_fantastic_four_first_steps/reviews","scoreLinkText":"390 Reviews"},"criticsTitle":"Tomatometer","criticsTop":{"averageRating":"6.70","certified":true,"likedCount":51,"notLikedCount":13,"ratingCount":64,"reviewCount":64,"score":"80","sentiment":"POSITIVE","reviewsPageUrl":"/m/the_fantastic_four_first_steps/reviews","scorePercent":"80%","title":"Tomatometer","scoreLinkUrl":"/m/the_fantastic_four_first_steps/reviews?type=top_critics","scoreLinkText":"64 Top Critic Reviews"},"hasAudienceAll":true,"hasAudienceVerified":true,"hasCriticsAll":true,"hasCriticsTop":true,"mediaType":"Movie","showScoreDetailsAudience":true,"learnMoreUrl":"https://editorial.rottentomatoes.com/article/introducing-verified-audience-score/"},"primaryImageUrl":"https://resizing.flixster.com/GRRDF-MY6_iS5Em5vNBg-Jd-uL0=/206x305/v2/https://resizing.flixster.com/WEfe-vTCqjHioD77J7f-qeoZZdY=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2FjN2M2NzA0LWNiMTctNDhjMy05N2VlLTk3YWI3N2JhZDZmMS5qcGc="} </script></media-scorecard-manager></div><section class="modules-nav" data-ModulesNavigationManager="navWrap"><modules-navigation-manager></modules-navigation-manager><nav><modules-navigation-carousel skeleton="panel" tilewidth="auto" data-ModulesNavigationManager="navCarousel"><a slot="tile" href="#what-to-know"><rt-tab data-ModulesNavigationManager="navTab">What to Know</rt-tab></a><a slot="tile" href="#critics-reviews"><rt-tab data-ModulesNavigationManager="navTab">Reviews</rt-tab></a><a slot="tile" href="#cast-and-crew"><rt-tab data-ModulesNavigationManager="navTab">Cast &amp; Crew</rt-tab></a><a slot="tile" href="#movie-clips"><rt-tab data-ModulesNavigationManager="navTab">Movie Clips</rt-tab></a><a slot="tile" href="#more-like-this"><rt-tab data-ModulesNavigationManager="navTab">More Like This</rt-tab></a><a slot="tile" href="#news-and-guides"><rt-tab data-ModulesNavigationManager="navTab">Related News</rt-tab></a><a slot="tile" href="#videos"><rt-tab data-ModulesNavigationManager="navTab">Videos</rt-tab></a><a slot="tile" href="#photos"><rt-tab data-ModulesNavigationManager="navTab">Photos</rt-tab></a><a slot="tile" href="#media-info"><rt-tab data-ModulesNavigationManager="navTab">Media Info</rt-tab></a></modules-navigation-carousel></nav></section><div class="modules-layout" tabindex="-1" data-ModulesNavigationManager="content:focusin"><div id="what-to-know" class="dom-anchor" data-ModulesNavigationManager="domAnchor"></div><section aria-labelledby="what-to-know-label" class="what-to-know" data-adobe-id="what-to-know" data-qa="section:what-to-know"><div class="header-wrap"><rt-text context="heading" size="0.75" style="--textColor: var(--grayDark4); --letterSpacing: 1px;--textTransform: capitalize;">The Fantastic Four: First Steps </rt-text><h2 class="unset" id="what-to-know-label"><rt-text context="heading" size="1.25" style="--textTransform: capitalize;">What to Know</rt-text></h2></div><div class="content"><div id="critics-consensus" class="consensus"><rt-text context="heading"><score-icon-critics certified="true" sentiment="POSITIVE" size="1"></score-icon-critics>Critics Consensus </rt-text><p>Benefitting from rock-solid cast chemistry and clad in appealingly retro 1960s design, this crack at <em>The Fantastic Four</em>does Marvel's First Family justice.</p><a href="/m/the_fantastic_four_first_steps/reviews">Read Critics Reviews</a></div><hr /><div id="audience-consensus" class="consensus"><rt-text context="heading"><score-icon-audience certified="true" size="1" sentiment="POSITIVE"></score-icon-audience>Audience Says </rt-text><p><em>The Fantastic Four</em>takes the world by Storm, Thing, Reed, Johnny and baby, forging a new path for this bespoke family that, with these <em>First Steps</em>, leaps into cosmic action with retro-futuristic verve.</p><a href="/m/the_fantastic_four_first_steps/reviews?type=user">Read Audience Reviews</a></div></div></section></div><ad-unit hidden unit-display="mobile" unit-type="interscroller" no-retry data-AdUnitManager="adUnit:interscrollerinstantiated"><aside slot="ad-inject" class="center mobile-interscroller"></aside></ad-unit><ad-unit hidden unit-display="desktop" unit-type="opbannerone"><div slot="ad-inject" class="banner-ad"></div></ad-unit><div class="modules-layout" tabindex="-1" data-ModulesNavigationManager="content:focusin"><div id="critics-reviews" class="dom-anchor" data-ModulesNavigationManager="domAnchor"></div><section aria-labelledby="critics-reviews-label" class="critics-reviews" data-adobe-id="critics-reviews" data-qa="section:critics-reviews"><div class="header-wrap"><h2 class="unset" id="critics-reviews-label"><rt-text size="1.25" context="heading" data-qa="title">Critics Reviews</rt-text></h2><rt-button arialabel="Critics Reviews" data-qa="view-all-link" href="/m/the_fantastic_four_first_steps/reviews" shape="pill" size="0.875" style="--buttonPadding: 6px 24px;--backgroundColor: var(--grayLight1);--borderColor: transparent;--letterSpacing: 1px;" theme="light">View More (390) </rt-button></div><div class="content-wrap"><carousel-slider tile-width="80%,45%" skeleton="panel" data-qa="carousel"><media-review-card-critic slot="tile" istopreview="true" data-qa="critic-review-tile"><rt-link aria-hidden="true" href="/critics/dwight-brown" slot="displayImage" style="--textColor: var(--grayDark2)" tabindex="-1" data-qa="critic-picture"><img src="https://resizing.flixster.com/n0vfm-A2s3Lwn9jGErs2wZJHwO0=/fit-in/128x128/v2/https://resizing.flixster.com/0ooK8MI8eZdyMLDFWIkNrt-1VPw=/128x128/v1.YzszODUxO2o7MjAzNDA7MjA0ODszMDA7MzAw" alt="Critic's profile" /></rt-link><rt-link href="/critics/dwight-brown" slot="displayName" style="--textColor: var(--grayDark2)" data-qa="critic-link"><rt-text context="label" size="0.875" style="--lineHeight: 1.25; --textColor: var(--grayDark3);">Dwight Brown </rt-text></rt-link><rt-link href="/critics/source/100009621" slot="publicationName" style="--textColor: var(--grayDark2)" data-qa="source-link"><rt-text size="0.75">DwightBrownInk.com </rt-text></rt-link><drawer-more maxlines="6" skeleton="panel" slot="reviewQuote" status="closed"><rt-text size="0.875" slot="content" data-qa="review-text">Audiences shouldnโ€™t have to sacrifice eye-catching stunts and imagery for great writing and acting. They should have it all. Should but wonโ€™t in this case. </rt-text></drawer-more><score-icon-critics sentiment="POSITIVE" size="1" slot="scoreIcon" verticalalign="sub"></score-icon-critics><rt-text size="0.75" slot="originalScore" style="--textColor: #62686F" data-qa="review-rating"><span>Rated: 2.5/4</span></rt-text><rt-text size="0.75" slot="createDate" style="--textColor: #62686F" data-qa="review-date"><span>Aug 18, 2025 </span></rt-text><rt-link href="https://dwightbrownink.com/the-fantastic-four-first-steps/" slot="editorialUrl" size="0.875" target="_blank" data-qa="full-review-link">Full Review</rt-link></media-review-card-critic><media-review-card-critic slot="tile" istopreview="true" data-qa="critic-review-tile"><rt-link aria-hidden="true" href="/critics/sergio-burstein" slot="displayImage" style="--textColor: var(--grayDark2)" tabindex="-1" data-qa="critic-picture"><img src="https://images.fandango.com/cms/assets/5b6ff500-1663-11ec-ae31-05a670d2d590--rtactordefault.png" alt="Critic's profile" /></rt-link><rt-link href="/critics/sergio-burstein" slot="displayName" style="--textColor: var(--grayDark2)" data-qa="critic-link"><rt-text context="label" size="0.875" style="--lineHeight: 1.25; --textColor: var(--grayDark3);">Sergio Burstein </rt-text></rt-link><rt-link href="/critics/source/268" slot="publicationName" style="--textColor: var(--grayDark2)" data-qa="source-link"><rt-text size="0.75">Los Angeles Times </rt-text></rt-link><drawer-more maxlines="6" skeleton="panel" slot="reviewQuote" status="closed"><rt-text size="0.875" slot="content" data-qa="review-text">For now, let&#39;s enjoy what this efficient commercial product has to offer, which surprisingly lasts less than two hours... [Full review in Spanish] </rt-text></drawer-more><score-icon-critics sentiment="POSITIVE" size="1" slot="scoreIcon" verticalalign="sub"></score-icon-critics><rt-text size="0.75" slot="originalScore" style="--textColor: #62686F" data-qa="review-rating"><span></span></rt-text><rt-text size="0.75" slot="createDate" style="--textColor: #62686F" data-qa="review-date"><span>Aug 11, 2025 </span></rt-text><rt-link href="https://www.latimes.com/espanol/entretenimiento/articulo/2025-07-24/criticas-unos-superheroes-del-pasado-un-vendedor-angustiado-y-otros-estrenos-de-cine" slot="editorialUrl" size="0.875" target="_blank" data-qa="full-review-link">Full Review</rt-link></media-review-card-critic><media-review-card-critic slot="tile" istopreview="true" data-qa="critic-review-tile"><rt-link aria-hidden="true" href="/critics/mark-kermode" slot="displayImage" style="--textColor: var(--grayDark2)" tabindex="-1" data-qa="critic-picture"><img src="https://images.fandango.com/cms/assets/5b6ff500-1663-11ec-ae31-05a670d2d590--rtactordefault.png" alt="Critic's profile" /></rt-link><rt-link href="/critics/mark-kermode" slot="displayName" style="--textColor: var(--grayDark2)" data-qa="critic-link"><rt-text context="label" size="0.875" style="--lineHeight: 1.25; --textColor: var(--grayDark3);">Mark Kermode </rt-text></rt-link><rt-link href="/critics/source/100009998" slot="publicationName" style="--textColor: var(--grayDark2)" data-qa="source-link"><rt-text size="0.75">Kermode and Mayo&#39;s Take (YouTube) </rt-text></rt-link><drawer-more maxlines="6" skeleton="panel" slot="reviewQuote" status="closed"><rt-text size="0.875" slot="content" data-qa="review-text">Even if you don&#39;t like the film, it is absolutely worth it for the furniture. </rt-text></drawer-more><score-icon-critics sentiment="POSITIVE" size="1" slot="scoreIcon" verticalalign="sub"></score-icon-critics><rt-text size="0.75" slot="originalScore" style="--textColor: #62686F" data-qa="review-rating"><span></span></rt-text><rt-text size="0.75" slot="createDate" style="--textColor: #62686F" data-qa="review-date"><span>Aug 6, 2025 </span></rt-text><rt-link href="https://www.youtube.com/watch?v=h1XYtl3vPX0" slot="editorialUrl" size="0.875" target="_blank" data-qa="full-review-link">Full Review</rt-link></media-review-card-critic><media-review-card-critic slot="tile" istopreview="false" data-qa="critic-review-tile"><rt-link aria-hidden="true" href="/critics/kevin-carr" slot="displayImage" style="--textColor: var(--grayDark2)" tabindex="-1" data-qa="critic-picture"><img src="https://resizing.flixster.com/0xcuvWu096RfySTVdXcZEFkAhAE=/fit-in/128x128/v2/https://resizing.flixster.com/UIrCTMQj3SyoFwW4g1XfMss1JkM=/38x54/v1.YzsxNzQ3O2o7MjAzNDA7MjA0ODszODs1NA" alt="Critic's profile" /></rt-link><rt-link href="/critics/kevin-carr" slot="displayName" style="--textColor: var(--grayDark2)" data-qa="critic-link"><rt-text context="label" size="0.875" style="--lineHeight: 1.25; --textColor: var(--grayDark3);">Kevin Carr </rt-text></rt-link><rt-link href="/critics/source/2722" slot="publicationName" style="--textColor: var(--grayDark2)" data-qa="source-link"><rt-text size="0.75">Fat Guys at the Movies </rt-text></rt-link><drawer-more maxlines="6" skeleton="panel" slot="reviewQuote" status="closed"><rt-text size="0.875" slot="content" data-qa="review-text">Itโ€™s still a superhero movie, and it connects to the next phase of Marvel crossover films. However, it taps into some real human elements. </rt-text></drawer-more><score-icon-critics sentiment="POSITIVE" size="1" slot="scoreIcon" verticalalign="sub"></score-icon-critics><rt-text size="0.75" slot="originalScore" style="--textColor: #62686F" data-qa="review-rating"><span>Rated: 4/5</span></rt-text><rt-text size="0.75" slot="createDate" style="--textColor: #62686F" data-qa="review-date"><span>Sep 4, 2025 </span></rt-text><rt-link href="https://www.fatguysatthemovies.com/the-fantastic-4-first-steps-movie-review/" slot="editorialUrl" size="0.875" target="_blank" data-qa="full-review-link">Full Review</rt-link></media-review-card-critic><media-review-card-critic slot="tile" istopreview="false" data-qa="critic-review-tile"><rt-link aria-hidden="true" href="/critics/niall-mccloskey" slot="displayImage" style="--textColor: var(--grayDark2)" tabindex="-1" data-qa="critic-picture"><img src="https://images.fandango.com/cms/assets/5b6ff500-1663-11ec-ae31-05a670d2d590--rtactordefault.png" alt="Critic's profile" /></rt-link><rt-link href="/critics/niall-mccloskey" slot="displayName" style="--textColor: var(--grayDark2)" data-qa="critic-link"><rt-text context="label" size="0.875" style="--lineHeight: 1.25; --textColor: var(--grayDark3);">Niall McCloskey </rt-text></rt-link><rt-link href="/critics/source/170" slot="publicationName" style="--textColor: var(--grayDark2)" data-qa="source-link"><rt-text size="0.75">Film Ireland Magazine </rt-text></rt-link><drawer-more maxlines="6" skeleton="panel" slot="reviewQuote" status="closed"><rt-text size="0.875" slot="content" data-qa="review-text">If youโ€™re looking for an electric superhero thrilling ride filled with compelling performances, great action and that classic comic flair, this may be four you. </rt-text></drawer-more><score-icon-critics sentiment="POSITIVE" size="1" slot="scoreIcon" verticalalign="sub"></score-icon-critics><rt-text size="0.75" slot="originalScore" style="--textColor: #62686F" data-qa="review-rating"><span></span></rt-text><rt-text size="0.75" slot="createDate" style="--textColor: #62686F" data-qa="review-date"><span>Sep 3, 2025 </span></rt-text><rt-link href="https://www.filmireland.net/review-the-fantastic-four-first-steps/" slot="editorialUrl" size="0.875" target="_blank" data-qa="full-review-link">Full Review</rt-link></media-review-card-critic><media-review-card-critic slot="tile" istopreview="false" data-qa="critic-review-tile"><rt-link aria-hidden="true" href="/critics/troy-ribeiro" slot="displayImage" style="--textColor: var(--grayDark2)" tabindex="-1" data-qa="critic-picture"><img src="https://resizing.flixster.com/hHWLBKUXqnNcIkcmP7YZiIBgeNc=/fit-in/128x128/v2/https://resizing.flixster.com/7Xxi64GEHd8sLpnSgyT6sPDGXKk=/128x128/v1.YzsxMDAwMDAzMjI2O2o7MjAzOTQ7MjA0ODsyMjcyOzE3MDQ" alt="Critic's profile" /></rt-link><rt-link href="/critics/troy-ribeiro" slot="displayName" style="--textColor: var(--grayDark2)" data-qa="critic-link"><rt-text context="label" size="0.875" style="--lineHeight: 1.25; --textColor: var(--grayDark3);">Troy Ribeiro </rt-text></rt-link><rt-link href="/critics/source/100010114" slot="publicationName" style="--textColor: var(--grayDark2)" data-qa="source-link"><rt-text size="0.75">Free Press Journal (India) </rt-text></rt-link><drawer-more maxlines="6" skeleton="panel" slot="reviewQuote" status="closed"><rt-text size="0.875" slot="content" data-qa="review-text">In The Fantastic Four: First Steps, Marvel returns to the drawing boardโ€”this time armed with chalk, retro optimism, and the ever-dependable Pedro Pascal </rt-text></drawer-more><score-icon-critics sentiment="POSITIVE" size="1" slot="scoreIcon" verticalalign="sub"></score-icon-critics><rt-text size="0.75" slot="originalScore" style="--textColor: #62686F" data-qa="review-rating"><span>Rated: 2.5/5</span></rt-text><rt-text size="0.75" slot="createDate" style="--textColor: #62686F" data-qa="review-date"><span>Aug 30, 2025 </span></rt-text><rt-link href="https://www.freepressjournal.in/entertainment/the-fantastic-four-first-steps-review-pedro-pascal-vanessa-kirby-ebon-moss-bachrach-joseph-quinn-and-julia-garner-take-first-steps-make-a-giant-stumble" slot="editorialUrl" size="0.875" target="_blank" data-qa="full-review-link">Full Review</rt-link></media-review-card-critic><tile-view-more aspect="fill" background="mediaHero" slot="tile"><rt-button href="/m/the_fantastic_four_first_steps/reviews" shape="pill" theme="transparent-lighttext">Read all reviews </rt-button></tile-view-more></carousel-slider></div></section></div><section aria-labelledby="audience-reviews-label" class="audience-reviews" data-adobe-id="audience-reviews" data-qa="section:audience-reviews"><div class="header-wrap"><h2 class="unset" id="audience-reviews-label"><rt-text size="1.25" context="heading" data-qa="title">Audience Reviews</rt-text></h2><rt-button arialabel="Audience Reviews" class="" data-qa="view-all-link" href="/m/the_fantastic_four_first_steps/reviews?type=user" shape="pill" size="0.875" style="--buttonPadding: 6px 24px;--backgroundColor: var(--grayLight1);--borderColor: transparent;--letterSpacing: 1px;" theme="light">View More (1000+) </rt-button></div><div class="content-wrap"><carousel-slider tile-width="80%,45%" skeleton="panel" data-qa="carousel"><media-review-card-audience slot="tile" isverifiedreview="true" data-qa="audience-review-tile"><rt-link context="label" href="" size="0.875" slot="displayName" style="--letterSpacing: 1px; --textColor: var(--grayDark3);" data-qa="user-name">Sanjeev </rt-link><drawer-more maxlines="4" skeleton="panel" slot="reviewQuote" status="closed"><rt-text size="0.875" slot="content" data-qa="review-text">Fantastic4 was okay. Choppy character development. Choppy story. </rt-text></drawer-more><rt-text size="0.75" slot="originalScore" data-qa="review-rating"><span aria-hidden="true">Rated 2/5 Stars &bull;&nbsp;</span><sr-text>Rated 2 out of 5 stars</sr-text></rt-text><rt-text size="0.75" slot="createDate" style="--textColor: #62686F" data-qa="review-date"><span>09/07/25</span></rt-text><rt-link data-MediaAudienceReviewsManager="fullReviewBtn:click" data-rating-id="ce6c7281-990c-4086-b5d8-91f0402daac7" size="0.875" slot="fullReviewBtn" data-qa="full-review-btn">Full Review </rt-link></media-review-card-audience><media-review-card-audience slot="tile" isverifiedreview="true" data-qa="audience-review-tile"><rt-link context="label" href="" size="0.875" slot="displayName" style="--letterSpacing: 1px; --textColor: var(--grayDark3);" data-qa="user-name">Daniel </rt-link><drawer-more maxlines="4" skeleton="panel" slot="reviewQuote" status="closed"><rt-text size="0.875" slot="content" data-qa="review-text">Could have benefitted by adding battles with Mole Man and a few of the older FF4 rogues gallery </rt-text></drawer-more><rt-text size="0.75" slot="originalScore" data-qa="review-rating"><span aria-hidden="true">Rated 4/5 Stars &bull;&nbsp;</span><sr-text>Rated 4 out of 5 stars</sr-text></rt-text><rt-text size="0.75" slot="createDate" style="--textColor: #62686F" data-qa="review-date"><span>09/07/25</span></rt-text><rt-link data-MediaAudienceReviewsManager="fullReviewBtn:click" data-rating-id="05699be7-4082-4ccd-9f00-100bca2b87d2" size="0.875" slot="fullReviewBtn" data-qa="full-review-btn">Full Review </rt-link></media-review-card-audience><media-review-card-audience slot="tile" isverifiedreview="true" data-qa="audience-review-tile"><rt-link context="label" href="" size="0.875" slot="displayName" style="--letterSpacing: 1px; --textColor: var(--grayDark3);" data-qa="user-name">Valentina </rt-link><drawer-more maxlines="4" skeleton="panel" slot="reviewQuote" status="closed"><rt-text size="0.875" slot="content" data-qa="review-text">The whole family enjoyed it ๐Ÿ˜ƒ </rt-text></drawer-more><rt-text size="0.75" slot="originalScore" data-qa="review-rating"><span aria-hidden="true">Rated 4/5 Stars &bull;&nbsp;</span><sr-text>Rated 4 out of 5 stars</sr-text></rt-text><rt-text size="0.75" slot="createDate" style="--textColor: #62686F" data-qa="review-date"><span>09/07/25</span></rt-text><rt-link data-MediaAudienceReviewsManager="fullReviewBtn:click" data-rating-id="0175adcb-18ab-461e-9230-762752a4edd4" size="0.875" slot="fullReviewBtn" data-qa="full-review-btn">Full Review </rt-link></media-review-card-audience><media-review-card-audience slot="tile" isverifiedreview="true" data-qa="audience-review-tile"><rt-link context="label" href="" size="0.875" slot="displayName" style="--letterSpacing: 1px; --textColor: var(--grayDark3);" data-qa="user-name">DUANE S </rt-link><drawer-more maxlines="4" skeleton="panel" slot="reviewQuote" status="closed"><rt-text size="0.875" slot="content" data-qa="review-text">It was surprisingly entertaining </rt-text></drawer-more><rt-text size="0.75" slot="originalScore" data-qa="review-rating"><span aria-hidden="true">Rated 4.5/5 Stars &bull;&nbsp;</span><sr-text>Rated 4.5 out of 5 stars</sr-text></rt-text><rt-text size="0.75" slot="createDate" style="--textColor: #62686F" data-qa="review-date"><span>09/07/25</span></rt-text><rt-link data-MediaAudienceReviewsManager="fullReviewBtn:click" data-rating-id="9be6f14a-d9af-4694-aec3-b2b9eabb7e49" size="0.875" slot="fullReviewBtn" data-qa="full-review-btn">Full Review </rt-link></media-review-card-audience><media-review-card-audience slot="tile" isverifiedreview="true" data-qa="audience-review-tile"><rt-link context="label" href="" size="0.875" slot="displayName" style="--letterSpacing: 1px; --textColor: var(--grayDark3);" data-qa="user-name">Tish </rt-link><drawer-more maxlines="4" skeleton="panel" slot="reviewQuote" status="closed"><rt-text size="0.875" slot="content" data-qa="review-text">it was good but I feel like I missed something. </rt-text></drawer-more><rt-text size="0.75" slot="originalScore" data-qa="review-rating"><span aria-hidden="true">Rated 4/5 Stars &bull;&nbsp;</span><sr-text>Rated 4 out of 5 stars</sr-text></rt-text><rt-text size="0.75" slot="createDate" style="--textColor: #62686F" data-qa="review-date"><span>09/07/25</span></rt-text><rt-link data-MediaAudienceReviewsManager="fullReviewBtn:click" data-rating-id="341b507a-56bc-4ba2-b414-3a39b480dd07" size="0.875" slot="fullReviewBtn" data-qa="full-review-btn">Full Review </rt-link></media-review-card-audience><media-review-card-audience slot="tile" isverifiedreview="true" data-qa="audience-review-tile"><rt-link context="label" href="" size="0.875" slot="displayName" style="--letterSpacing: 1px; --textColor: var(--grayDark3);" data-qa="user-name">David L </rt-link><drawer-more maxlines="4" skeleton="panel" slot="reviewQuote" status="closed"><rt-text size="0.875" slot="content" data-qa="review-text">Way too much back story and not enough action. Kept waiting for something to happen. </rt-text></drawer-more><rt-text size="0.75" slot="originalScore" data-qa="review-rating"><span aria-hidden="true">Rated 1/5 Stars &bull;&nbsp;</span><sr-text>Rated 1 out of 5 stars</sr-text></rt-text><rt-text size="0.75" slot="createDate" style="--textColor: #62686F" data-qa="review-date"><span>09/07/25</span></rt-text><rt-link data-MediaAudienceReviewsManager="fullReviewBtn:click" data-rating-id="82dd27ed-86cb-471c-b791-a73b52cda4b5" size="0.875" slot="fullReviewBtn" data-qa="full-review-btn">Full Review </rt-link></media-review-card-audience><tile-view-more aspect="fill" background="mediaHero" slot="tile"><rt-button href="/m/the_fantastic_four_first_steps/reviews?type=user" shape="pill" theme="transparent-lighttext">Read all reviews </rt-button></tile-view-more></carousel-slider></div><media-audience-reviews-manager><script type="application/json" data-json="reviewsData">{"audienceScore":{"certifiedFresh":"certified","reviewCount":5804,"score":"91","sentiment":"POSITIVE","certified":true,"scorePercent":"91%"},"criticsScore":{"certified":true,"score":"87","sentiment":"POSITIVE","scorePercent":"87%"},"emptyMessage":"There are no Verified Audience reviews for The Fantastic Four: First Steps yet.","linkCss":"","partial":"pages/_shared/mediaAudienceReviewsCarousel.html","ratingsData":{"emsId":"db64beee-683b-39ce-9617-94f6b67aa997","isPreRelease":false},"reviews":[{"displayDate":"09/07/25","displayName":"Sanjeev","isVerified":true,"ratingId":"ce6c7281-990c-4086-b5d8-91f0402daac7","ratingRange":"Rated 2/5 Stars &bull;&nbsp;","ratingRangeA11y":"Rated 2 out of 5 stars","review":"Fantastic4 was okay. Choppy character development. Choppy story."},{"displayDate":"09/07/25","displayName":"Daniel","isVerified":true,"ratingId":"05699be7-4082-4ccd-9f00-100bca2b87d2","ratingRange":"Rated 4/5 Stars &bull;&nbsp;","ratingRangeA11y":"Rated 4 out of 5 stars","review":"Could have benefitted by adding battles with Mole Man and a few of the older FF4 rogues gallery"},{"displayDate":"09/07/25","displayName":"Valentina","isVerified":true,"ratingId":"0175adcb-18ab-461e-9230-762752a4edd4","ratingRange":"Rated 4/5 Stars &bull;&nbsp;","ratingRangeA11y":"Rated 4 out of 5 stars","review":"The whole family enjoyed it ๐Ÿ˜ƒ"},{"displayDate":"09/07/25","displayName":"DUANE S","isVerified":true,"ratingId":"9be6f14a-d9af-4694-aec3-b2b9eabb7e49","ratingRange":"Rated 4.5/5 Stars &bull;&nbsp;","ratingRangeA11y":"Rated 4.5 out of 5 stars","review":"It was surprisingly entertaining"},{"displayDate":"09/07/25","displayName":"Tish","isVerified":true,"ratingId":"341b507a-56bc-4ba2-b414-3a39b480dd07","ratingRange":"Rated 4/5 Stars &bull;&nbsp;","ratingRangeA11y":"Rated 4 out of 5 stars","review":"it was good but I feel like I missed something."},{"displayDate":"09/07/25","displayName":"David L","isVerified":true,"ratingId":"82dd27ed-86cb-471c-b791-a73b52cda4b5","ratingRange":"Rated 1/5 Stars &bull;&nbsp;","ratingRangeA11y":"Rated 1 out of 5 stars","review":"Way too much back story and not enough action. Kept waiting for something to happen."}],"reviewCount":5804,"reviewsUrl":"/m/the_fantastic_four_first_steps/reviews?type=user","title":"The Fantastic Four: First Steps","viewMoreText":"View More (1000+)"}</script></media-audience-reviews-manager></section><section aria-labelledby="rate-and-review-label" class="rate-and-review" data-adobe-id="rate-and-review" data-qa="section:rate-and-review"><rate-and-review-module-manager><script data-json="rateAndReviewModule" type="application/json">{"emsId":"db64beee-683b-39ce-9617-94f6b67aa997","releaseDate":"Jul 25, 2025","mediaType":"movie","title":"The Fantastic Four: First Steps"}</script></rate-and-review-module-manager><div class="header-wrap"><rt-text context="heading" size="0.75" style="--textColor: #62686F; --letterSpacing: 1px; --textTransform: capitalize;">The Fantastic Four: First Steps </rt-text><h2 class="unset" id="rate-and-review-label"><rt-text size="1.25" context="heading">My Rating</rt-text></h2></div><div class="content"><rate-and-review-module data-RateAndReviewModuleManager="rateAndReviewModule" skeleton="panel" status="unrated"><rating-stars-group data-RateAndReviewModuleManager="stars:changed" data-RateAndReviewOverlayManager="moduleStars" aria-labelledby="ratingStarsLabel" is-selectable size="2.75,2" slot="rating"></rating-stars-group><rating-descriptions context="label" data-RateAndReviewModuleManager="ratingDescriptions" size="1" slot="description" hidden></rating-descriptions><drawer-more maxlines="2" slot="review-quote" status="closed"><rt-text data-RateAndReviewModuleManager="userReview" data-RateAndReviewOverlayManager="moduleReview" size="0.875" slot="content"></rt-text><rt-link slot="ctaOpen" size="0.875" context="label">Read More</rt-link><rt-link slot="ctaClose" size="0.875" context="label">Read Less</rt-link></drawer-more><rt-button data-RateAndReviewModuleManager="rateBtn:click" shape="pill" size="1" slot="cta-rate">POST RATING </rt-button><rt-button data-RateAndReviewModuleManager="writeReviewBtn:click" size="1" slot="cta-review" theme="transparent">WRITE A REVIEW </rt-button><rt-button data-RateAndReviewModuleManager="editReviewBtn:click" size="1" slot="cta-edit" theme="transparent">EDIT REVIEW </rt-button></rate-and-review-module></div><rate-and-review-overlay-manager data-RateAndReviewModuleManager="overlayManager:error,success"></rate-and-review-overlay-manager></section><div class="modules-layout" tabindex="-1" data-ModulesNavigationManager="content:focusin"><div id="cast-and-crew" class="dom-anchor" data-ModulesNavigationManager="domAnchor"></div><section aria-labelledby="cast-and-crew-label" class="cast-and-crew" data-adobe-id="cast-and-crew" data-qa="section:cast-and-crew"><div class="header-wrap"><h2 class="unset" id="cast-and-crew-label"><rt-text size="1.25" context="heading" data-qa="title">Cast & Crew</rt-text></h2><rt-button arialabel="Cast and Crew" data-qa="view-all-link" href="/m/the_fantastic_four_first_steps/cast-and-crew" shape="pill" size="0.875" style="--buttonPadding: 6px 24px;--backgroundColor: var(--grayLight1);--borderColor: transparent;--letterSpacing: 1px;" theme="light">View All </rt-button></div><div class="content-wrap"><a href="/celebrity/matt-shakman" data-qa="person-item"><tile-dynamic skeleton="panel"><rt-img alt="Matt Shakman thumbnail image" aria-hidden="true" loading="lazy" slot="image" src="https://resizing.flixster.com/GEC1uAUpUH1wNcCsixMU3Id9Y-I=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/103981_v9_bb.jpg"></rt-img><div slot="insetText" aria-label="Matt Shakman, Director"><p class="name" data-qa="person-name">Matt Shakman</p><p class="role" data-qa="person-role">Director</p></div></tile-dynamic></a><a href="/celebrity/pedro_pascal" data-qa="person-item"><tile-dynamic skeleton="panel"><rt-img alt="Pedro Pascal thumbnail image" aria-hidden="true" loading="lazy" slot="image" src="https://resizing.flixster.com/gHGci208eTBBe6s555qqrVvMAlg=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/494807_v9_bd.jpg"></rt-img><div slot="insetText" aria-label="Pedro Pascal, Reed Richards "><p class="name" data-qa="person-name">Pedro Pascal</p><p class="role" data-qa="person-role">Reed Richards </p></div></tile-dynamic></a><a href="/celebrity/vanessa_kirby" data-qa="person-item"><tile-dynamic skeleton="panel"><rt-img alt="Vanessa Kirby thumbnail image" aria-hidden="true" loading="lazy" slot="image" src="https://resizing.flixster.com/hYfFokzVjtDj7QBvMrYIJ5uAhmY=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/631337_v9_bb.jpg"></rt-img><div slot="insetText" aria-label="Vanessa Kirby, Sue Storm "><p class="name" data-qa="person-name">Vanessa Kirby</p><p class="role" data-qa="person-role">Sue Storm </p></div></tile-dynamic></a><a href="/celebrity/ebon_moss_bachrach" data-qa="person-item"><tile-dynamic skeleton="panel"><rt-img alt="Ebon Moss-Bachrach thumbnail image" aria-hidden="true" loading="lazy" slot="image" src="https://resizing.flixster.com/cEb3kX_Yn_L4trv76D6rNON5nLA=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/252022_v9_bc.jpg"></rt-img><div slot="insetText" aria-label="Ebon Moss-Bachrach, Ben Grimm "><p class="name" data-qa="person-name">Ebon Moss-Bachrach</p><p class="role" data-qa="person-role">Ben Grimm </p></div></tile-dynamic></a><a href="/celebrity/joseph_quinn" data-qa="person-item"><tile-dynamic skeleton="panel"><rt-img alt="Joseph Quinn thumbnail image" aria-hidden="true" loading="lazy" slot="image" src="https://resizing.flixster.com/mwGK9WCbDyzD9FKvC81OyaWTfjc=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/1102755_v9_bb.jpg"></rt-img><div slot="insetText" aria-label="Joseph Quinn, Johnny Storm "><p class="name" data-qa="person-name">Joseph Quinn</p><p class="role" data-qa="person-role">Johnny Storm </p></div></tile-dynamic></a><a href="/celebrity/ralph-ineson" data-qa="person-item"><tile-dynamic skeleton="panel"><rt-img alt="Ralph Ineson thumbnail image" aria-hidden="true" loading="lazy" slot="image" src="https://resizing.flixster.com/-UyEiZ3UKHhGzdQU4wDV10Z6wO0=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/268419_v9_bc.jpg"></rt-img><div slot="insetText" aria-label="Ralph Ineson, Galactus"><p class="name" data-qa="person-name">Ralph Ineson</p><p class="role" data-qa="person-role">Galactus</p></div></tile-dynamic></a></div></section></div><div class="modules-layout" tabindex="-1" data-ModulesNavigationManager="content:focusin"><div id="movie-clips" class="dom-anchor" data-ModulesNavigationManager="domAnchor"></div><section aria-labelledby="movie-clips-label" class="movie-clips" data-adobe-id="movie-clips" data-qa="section:movie-clips"><div class="header-wrap"><div class="link-wrap"><h2 class="unset" id="movie-clips-label"><rt-text size="1.25" context="heading">Movie Clips</rt-text></h2><rt-button arialabel=" videos" data-qa="videos-view-all-link" href="/m/the_fantastic_four_first_steps/videos" shape="pill" size="0.875" style="--buttonPadding: 6px 24px;--backgroundColor: var(--grayLight1);--borderColor: transparent;--letterSpacing: 1px;" theme="light">View All </rt-button></div><h3 class="unset"><rt-text context="heading" size="0.75" style="--letterSpacing: 1px; --textColor: var(--grayDark4); --textTransform: capitalize;">The Fantastic Four: First Steps </rt-text></h3></div><carousel-slider tile-width="80%,240px" data-VideosCarouselManager="carousel" skeleton="panel" data-qa="videos-carousel"><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" loading="" src="https://resizing.flixster.com/2ckR7vRcYqkP8nqhQWY7WPZHDRA=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/496/667/thumb_0598F689-083F-4438-9A8D-C95478298C2F.jpg" alt="The Fantastic Four: First Steps: Movie Clip - I Herald Galactus "></rt-img><rt-button theme="transparent" data-ems-id="db64beee-683b-39ce-9617-94f6b67aa997" data-mpx-id="2437913667638" data-public-id="xE7nTGsSlCZU" data-type="Movie" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">The Fantastic Four: First Steps: Movie Clip - I Herald Galactus</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">The Fantastic Four: First Steps: Movie Clip - I Herald Galactus</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">1:01 </rt-badge></tile-video><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" loading="" src="https://resizing.flixster.com/o_5MtdtAJJdYl_U005ycG4AbHoM=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/273/518/thumb_ABC39A90-FC12-4796-9781-7821A147C50E.jpg" alt="The Fantastic Four: First Steps: Movie Clip - Sunday Dinner "></rt-img><rt-button theme="transparent" data-ems-id="db64beee-683b-39ce-9617-94f6b67aa997" data-mpx-id="2437679683546" data-public-id="rWBzo2Bw49o3" data-type="Movie" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">The Fantastic Four: First Steps: Movie Clip - Sunday Dinner</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">The Fantastic Four: First Steps: Movie Clip - Sunday Dinner</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">1:24 </rt-badge></tile-video><tile-view-more aspect="landscape" background="mediaHero" slot="tile"><rt-button href="/m/the_fantastic_four_first_steps/videos" shape="pill" theme="transparent-lighttext">View more videos </rt-button></tile-view-more></carousel-slider></section></div><div class="modules-layout" tabindex="-1" data-ModulesNavigationManager="content:focusin"><div id="more-like-this" class="dom-anchor" data-ModulesNavigationManager="domAnchor"></div><section aria-labelledby="more-like-this-label" class="more-like-this" data-adobe-id="more-like-this" data-qa="section:more-like-this"><div class="header-wrap"><div class="link-wrap"><h3 class="unset" id="more-like-this-label"><rt-text size="1.25" context="heading">More Like This </rt-text></h3><rt-button arialabel="Movies in Theaters" data-qa="view-all-link" href="/browse/movies_in_theaters/sort:popular" shape="pill" size="0.875" style="--buttonPadding: 6px 24px;--backgroundColor: var(--grayLight1);--borderColor: transparent;--letterSpacing: 1px;" theme="light">View All </rt-button></div></div><div class="content-wrap"><carousel-slider skeleton="panel" tile-width="140px" gap="15px"><tile-poster-card slot="tile"><rt-link slot="primaryImage" href="/m/godzilla_vs_kong" tabindex="-1"><sr-text>Godzilla vs. Kong</sr-text><rt-img loading="" src="https://resizing.flixster.com/EEYz8hv8oK7__RZ-gAD9TaU9v1Y=/206x305/v2/https://resizing.flixster.com/tZyAodV5kbRWTPtyiBo71sOSee8=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzYzYThhNTAwLTdlZTAtNDM3MS1hNmEyLTdlNzczNWVjNmE2YS5qcGc=" alt="Godzilla vs. Kong poster"></rt-img></rt-link><score-icon-critics certified="true" sentiment="POSITIVE" size="1" slot="criticsIcon" verticalalign="sub"></score-icon-critics><rt-text slot="criticsScore" size="0.9" context="label">76% </rt-text><score-icon-audience certified="true" sentiment="POSITIVE" size="1" slot="audienceIcon"></score-icon-audience><rt-text slot="audienceScore" size="0.9" context="label">91% </rt-text><rt-link slot="title" href="/m/godzilla_vs_kong" size="0.85" context="label">Godzilla vs. Kong </rt-link><watchlist-button data-WatchlistButtonManager="watchlistButton:click" emsid="04bd0a06-366b-39cb-a3f5-3d26aad6d986" mediatype="Movie" mediatitle="Godzilla vs. Kong" slot="watchlistButton" state="unchecked"><span slot="text">Watchlist</span></watchlist-button><rt-button data-content-type="PROMO" data-disable-ads="" data-ems-id="04bd0a06-366b-39cb-a3f5-3d26aad6d986" data-mpx-id="1847861827818" data-position="1" data-public-id="_Fg3QpNe3EO4" data-title="Godzilla vs. Kong: Trailer 1" data-track="poster" data-type="Movie" data-VideoPlayerOverlayManager="btnVideo:click" data-video-list="" slot="trailerButton" size="0.875" theme="transparent"><rt-icon icon="play"></rt-icon><span>TRAILER</span><sr-text>for Godzilla vs. Kong</sr-text></rt-button></tile-poster-card><tile-poster-card slot="tile"><rt-link slot="primaryImage" href="/m/avengers_infinity_war" tabindex="-1"><sr-text>Avengers: Infinity War</sr-text><rt-img loading="" src="https://resizing.flixster.com/xC06wd4ol1CFog1PZ34PRcr_akg=/206x305/v2/https://resizing.flixster.com/CXOXbOpLNL1NNkXTQu-4Rgvcszs=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzM0NGRkMDM2LWVjNDQtNGZlMC04NGM3LWZkMzQ2Njg1OTUyNi53ZWJw" alt="Avengers: Infinity War poster"></rt-img></rt-link><score-icon-critics certified="true" sentiment="POSITIVE" size="1" slot="criticsIcon" verticalalign="sub"></score-icon-critics><rt-text slot="criticsScore" size="0.9" context="label">85% </rt-text><score-icon-audience certified="false" sentiment="POSITIVE" size="1" slot="audienceIcon"></score-icon-audience><rt-text slot="audienceScore" size="0.9" context="label">92% </rt-text><rt-link slot="title" href="/m/avengers_infinity_war" size="0.85" context="label">Avengers: Infinity War </rt-link><watchlist-button data-WatchlistButtonManager="watchlistButton:click" emsid="8ef022ef-f88a-33c8-8f6e-ab87f5039eea" mediatype="Movie" mediatitle="Avengers: Infinity War" slot="watchlistButton" state="unchecked"><span slot="text">Watchlist</span></watchlist-button><rt-button data-content-type="PROMO" data-disable-ads="" data-ems-id="8ef022ef-f88a-33c8-8f6e-ab87f5039eea" data-mpx-id="1187576387953" data-position="2" data-public-id="VsE2xAyymQFk" data-title="Avengers: Infinity War: Trailer 2" data-track="poster" data-type="Movie" data-VideoPlayerOverlayManager="btnVideo:click" data-video-list="" slot="trailerButton" size="0.875" theme="transparent"><rt-icon icon="play"></rt-icon><span>TRAILER</span><sr-text>for Avengers: Infinity War</sr-text></rt-button></tile-poster-card><tile-poster-card slot="tile"><rt-link slot="primaryImage" href="/m/war_for_the_planet_of_the_apes" tabindex="-1"><sr-text>War for the Planet of the Apes</sr-text><rt-img loading="" src="https://resizing.flixster.com/hzbgPAMhNcS60Ig1L2yRcCfuB0A=/206x305/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p12126545_p_v8_ar.jpg" alt="War for the Planet of the Apes poster"></rt-img></rt-link><score-icon-critics certified="true" sentiment="POSITIVE" size="1" slot="criticsIcon" verticalalign="sub"></score-icon-critics><rt-text slot="criticsScore" size="0.9" context="label">94% </rt-text><score-icon-audience certified="false" sentiment="POSITIVE" size="1" slot="audienceIcon"></score-icon-audience><rt-text slot="audienceScore" size="0.9" context="label">84% </rt-text><rt-link slot="title" href="/m/war_for_the_planet_of_the_apes" size="0.85" context="label">War for the Planet of the Apes </rt-link><watchlist-button data-WatchlistButtonManager="watchlistButton:click" emsid="58230728-f14f-3d9d-8163-c42f77862c12" mediatype="Movie" mediatitle="War for the Planet of the Apes" slot="watchlistButton" state="unchecked"><span slot="text">Watchlist</span></watchlist-button><rt-button data-content-type="PROMO" data-disable-ads="" data-ems-id="58230728-f14f-3d9d-8163-c42f77862c12" data-mpx-id="977018435843" data-position="3" data-public-id="6uDzUM2KPOVC" data-title="War for the Planet of the Apes: Trailer 4" data-track="poster" data-type="Movie" data-VideoPlayerOverlayManager="btnVideo:click" data-video-list="" slot="trailerButton" size="0.875" theme="transparent"><rt-icon icon="play"></rt-icon><span>TRAILER</span><sr-text>for War for the Planet of the Apes</sr-text></rt-button></tile-poster-card><tile-poster-card slot="tile"><rt-link slot="primaryImage" href="/m/godzilla_minus_one" tabindex="-1"><sr-text>Godzilla Minus One</sr-text><rt-img loading="" src="https://resizing.flixster.com/54ve7LYDF04gM_uczrFs6oG2zTw=/206x305/v2/https://resizing.flixster.com/CDYB6aGzmamA9BCmveGRQ880KRs=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzk5MmIwMzZiLWU2ZGMtNDU4NC04YTYxLTA3YmRlMDVkYjI3YS5qcGc=" alt="Godzilla Minus One poster"></rt-img></rt-link><score-icon-critics certified="true" sentiment="POSITIVE" size="1" slot="criticsIcon" verticalalign="sub"></score-icon-critics><rt-text slot="criticsScore" size="0.9" context="label">99% </rt-text><score-icon-audience certified="true" sentiment="POSITIVE" size="1" slot="audienceIcon"></score-icon-audience><rt-text slot="audienceScore" size="0.9" context="label">98% </rt-text><rt-link slot="title" href="/m/godzilla_minus_one" size="0.85" context="label">Godzilla Minus One </rt-link><watchlist-button data-WatchlistButtonManager="watchlistButton:click" emsid="90c22bee-a1dd-44f6-8345-d1792b4dddc3" mediatype="Movie" mediatitle="Godzilla Minus One" slot="watchlistButton" state="unchecked"><span slot="text">Watchlist</span></watchlist-button><rt-button data-content-type="PROMO" data-disable-ads="" data-ems-id="90c22bee-a1dd-44f6-8345-d1792b4dddc3" data-mpx-id="2383603779658" data-position="4" data-public-id="Mjli8tOlPmKB" data-title="Godzilla Minus One: Re-Release Trailer" data-track="poster" data-type="Movie" data-VideoPlayerOverlayManager="btnVideo:click" data-video-list="" slot="trailerButton" size="0.875" theme="transparent"><rt-icon icon="play"></rt-icon><span>TRAILER</span><sr-text>for Godzilla Minus One</sr-text></rt-button></tile-poster-card><tile-poster-card slot="tile"><rt-link slot="primaryImage" href="/m/the_creator_2023" tabindex="-1"><sr-text>The Creator</sr-text><rt-img loading="" src="https://resizing.flixster.com/a8yOHUJZN88nlYhy8BTgM1gxNbI=/206x305/v2/https://resizing.flixster.com/CqeQShqSXJ8Ec-2I9RYEXdoncH0=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzNjYzAzM2ZiLTc5ZGItNDUyYS05MmFkLTY5NDRiYjRiODlkYi5qcGc=" alt="The Creator poster"></rt-img></rt-link><score-icon-critics certified="false" sentiment="POSITIVE" size="1" slot="criticsIcon" verticalalign="sub"></score-icon-critics><rt-text slot="criticsScore" size="0.9" context="label">67% </rt-text><score-icon-audience certified="false" sentiment="POSITIVE" size="1" slot="audienceIcon"></score-icon-audience><rt-text slot="audienceScore" size="0.9" context="label">75% </rt-text><rt-link slot="title" href="/m/the_creator_2023" size="0.85" context="label">The Creator </rt-link><watchlist-button data-WatchlistButtonManager="watchlistButton:click" emsid="7550c7be-4166-3c4c-a2dc-03a223aa3b29" mediatype="Movie" mediatitle="The Creator" slot="watchlistButton" state="unchecked"><span slot="text">Watchlist</span></watchlist-button><rt-button data-content-type="PROMO" data-disable-ads="" data-ems-id="7550c7be-4166-3c4c-a2dc-03a223aa3b29" data-mpx-id="2263363651869" data-position="5" data-public-id="UYuDwo6HZ1BN" data-title="The Creator: Final Trailer" data-track="poster" data-type="Movie" data-VideoPlayerOverlayManager="btnVideo:click" data-video-list="" slot="trailerButton" size="0.875" theme="transparent"><rt-icon icon="play"></rt-icon><span>TRAILER</span><sr-text>for The Creator</sr-text></rt-button></tile-poster-card><tile-poster-card skeleton="panel" slot="tile" tabindex="-1"><tile-view-more aspect="posterCard" background="collage" slot="primaryImage"></tile-view-more><rt-text slot="title" size="0.85" context="label">Discover more movies and TV shows.</rt-text><rt-button href="/browse/movies_in_theaters/sort:popular" slot="watchlistButton" shape="pill" size="0.875" theme="transparent-darktext" aria-label="View More Movies in Theaters">View More </rt-button></tile-poster-card></carousel-slider></div></section></div><div class="modules-layout" tabindex="-1" data-ModulesNavigationManager="content:focusin"><div id="news-and-guides" class="dom-anchor" data-ModulesNavigationManager="domAnchor"></div><section aria-labelledby="news-and-guides-label" class="news-and-guides" data-adobe-id="news-and-guides" data-qa="section:news-and-guides"><div class="header-wrap"><div class="link-wrap"><h2 class="unset" id="news-and-guides-label"><rt-text size="1.25" style="--textTransform: capitalize;" context="heading" data-qa="title">Related Movie News</rt-text></h2><rt-button arialabel="Related Movie News" data-qa="view-all-link" href="https://editorial.rottentomatoes.com/more-related-content/?relatedmovieid=db64beee-683b-39ce-9617-94f6b67aa997" shape="pill" size="0.875" style="--buttonPadding: 6px 24px;--backgroundColor: var(--grayLight1);--borderColor: transparent;--letterSpacing: 1px;" theme="light">View All </rt-button></div></div><div class="content-wrap"><carousel-slider tile-width="80%,240px" skeleton="panel" data-qa="carousel"><a slot="tile" href="https://editorial.rottentomatoes.com/article/the-most-anticipated-movies-of-2025/" data-qa="article"><tile-dynamic orientation="landscape" skeleton="panel"><rt-img slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2025/08/Most_Anticipated_Movies_2025_Running_Man-Rep.jpg" loading="lazy"></rt-img><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="article-title">The Most Anticipated Movies of 2025</rt-text></drawer-more></tile-dynamic></a><a slot="tile" href="https://editorial.rottentomatoes.com/article/12-plot-threads-marvel-still-needs-to-tie-up-after-the-fantastic-four-first-steps/" data-qa="article"><tile-dynamic orientation="landscape" skeleton="panel"><rt-img slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2025/07/MCU_Plots-Rep.jpg" loading="lazy"></rt-img><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="article-title">12 Plot Threads Marvel Still Needs to Tie Up after <em>The Fantastic Four: First Steps</em></rt-text></drawer-more></tile-dynamic></a><a slot="tile" href="https://editorial.rottentomatoes.com/article/weekend-box-office-fantastic-four-scores-big-win-for-marvel/" data-qa="article"><tile-dynamic orientation="landscape" skeleton="panel"><rt-img slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2025/07/Fantastic_Four_First_Steps_BO1-Rep.jpg" loading="lazy"></rt-img><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="article-title">Weekend Box Office: <em>The Fantastic Four</em>Scores a Big Win for Marvel</rt-text></drawer-more></tile-dynamic></a></carousel-slider></div></section></div><div class="modules-layout" tabindex="-1" data-ModulesNavigationManager="content:focusin"><div id="videos" class="dom-anchor" data-ModulesNavigationManager="domAnchor"></div><section aria-labelledby="videos-carousel-label" class="videos-carousel" data-adobe-id="videos-carousel" data-qa="section:videos-carousel"><div class="header-wrap"><div class="link-wrap"><h2 class="unset" data-qa="videos-section-title" id="videos-carousel-label"><rt-text size="1.25" context="heading">Videos</rt-text></h2><rt-button arialabel=" videos" data-qa="videos-view-all-link" href="/m/the_fantastic_four_first_steps/videos" shape="pill" size="0.875" style="--buttonPadding: 6px 24px;--backgroundColor: var(--grayLight1);--borderColor: transparent;--letterSpacing: 1px;" theme="light">View All </rt-button></div><h3 class="unset"><rt-text context="heading" size="0.75" style="--letterSpacing: 1px; --textColor: var(--grayDark4); --textTransform: capitalize;">The Fantastic Four: First Steps </rt-text></h3></div><carousel-slider tile-width="80%,240px" data-VideosCarouselManager="carousel" skeleton="panel" data-qa="videos-carousel"><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="" src="https://resizing.flixster.com/8pI5zoGYNLsqkovBikK9vs0Q_dE=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/663/931/thumb_4ca6d6c5-6a62-11f0-94b5-022bbbb30d69.jpg" alt="The Fantastic Four: First Steps: 4 Us All"></rt-img><rt-button theme="transparent" data-ems-id="db64beee-683b-39ce-9617-94f6b67aa997" data-mpx-id="2441310275805" data-public-id="BBgYzBvAIQDN" data-type="Movie" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">The Fantastic Four: First Steps: 4 Us All</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">The Fantastic Four: First Steps: 4 Us All</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">0:59 </rt-badge></tile-video><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="" src="https://resizing.flixster.com/XZ_r9FybdPB8iwk74gCKy5aYSaU=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/555/522/thumb_27A9CD85-8C9C-46FC-BE15-5621BD2088B3.jpg" alt="The Fantastic Four Draft Their MCU Fantasy Team"></rt-img><rt-button theme="transparent" data-ems-id="db64beee-683b-39ce-9617-94f6b67aa997" data-mpx-id="2441196611548" data-public-id="dKENBs1zPSMQ" data-type="Movie" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">The Fantastic Four Draft Their MCU Fantasy Team</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">The Fantastic Four Draft Their MCU Fantasy Team</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">1:15 </rt-badge></tile-video><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="" src="https://resizing.flixster.com/TFtzxS3dV1GMq0Zb5h8avmCaTI0=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/551/119/thumb_41241D95-C081-4422-962E-7A588798E2C2.jpg" alt="How is Galactus as a Boss?"></rt-img><rt-button theme="transparent" data-ems-id="db64beee-683b-39ce-9617-94f6b67aa997" data-mpx-id="2441192003818" data-public-id="ky_Lk6vXvywy" data-type="Movie" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">How is Galactus as a Boss?</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">How is Galactus as a Boss?</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">0:53 </rt-badge></tile-video><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="" src="https://resizing.flixster.com/V-7Y5EWNCCikxneT-APVv4rQ8pw=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/551/619/thumb_5E7365E1-5257-4EE5-97C9-D8EDE5B21724.jpg" alt="The Fantastic Four: First Steps: TV Spot - Best Team"></rt-img><rt-button theme="transparent" data-ems-id="db64beee-683b-39ce-9617-94f6b67aa997" data-mpx-id="2441192515681" data-public-id="8kSEbEu1Zhgw" data-type="Movie" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">The Fantastic Four: First Steps: TV Spot - Best Team</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">The Fantastic Four: First Steps: TV Spot - Best Team</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">0:42 </rt-badge></tile-video><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="" src="https://resizing.flixster.com/CyvZwgalYR4RpkWZ3JZAnY58zRU=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/553/71/thumb_A18EC82A-C972-4A45-9F62-94B441AB73D7.jpg" alt="Who Is the Glue of the #FantasticFour?"></rt-img><rt-button theme="transparent" data-ems-id="db64beee-683b-39ce-9617-94f6b67aa997" data-mpx-id="2441194051608" data-public-id="O22eNh1x9EXr" data-type="Movie" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">Who Is the Glue of the #FantasticFour?</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">Who Is the Glue of the #FantasticFour?</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">1:12 </rt-badge></tile-video><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="lazy" src="https://resizing.flixster.com/Kae7Hpw4wvBW8mScdFxwgl4HDA4=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/546/239/thumb_E96B5C4E-11FD-44E8-ABBC-57DEF64D9182.jpg" alt="The Herald Scene on DAY 1?! #FantasticFour"></rt-img><rt-button theme="transparent" data-ems-id="db64beee-683b-39ce-9617-94f6b67aa997" data-mpx-id="2441186883850" data-public-id="b7daKHSpDg_G" data-type="Movie" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">The Herald Scene on DAY 1?! #FantasticFour</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">The Herald Scene on DAY 1?! #FantasticFour</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">1:09 </rt-badge></tile-video><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="lazy" src="https://resizing.flixster.com/ywU2R45-iE85jfOACE0QqIAEvPw=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/357/275/thumb_53CCBA70-92E5-46A7-86BA-9C9A4666249B.jpg" alt="The Cast of &#39;The Fantastic Four: First Steps&#39; Talk Family Reunion, Heralding Galactus, and More!"></rt-img><rt-button theme="transparent" data-ems-id="db64beee-683b-39ce-9617-94f6b67aa997" data-mpx-id="2440988739926" data-public-id="tcGVvjLsPP54" data-type="Movie" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">The Cast of &#39;The Fantastic Four: First Steps&#39; Talk Family Reunion, Heralding Galactus, and More!</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">The Cast of &#39;The Fantastic Four: First Steps&#39; Talk Family Reunion, Heralding Galactus, and More!</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">23:37 </rt-badge></tile-video><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="lazy" src="https://resizing.flixster.com/E_WEVGUx0sdH8xGrMdgNW15RICc=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/322/615/thumb_1398D733-67C4-4CB3-8512-4094F4FF5D95.jpg" alt="Meet Marvel&#39;s First Family in The Fantastic Four: First Steps, now playing in theaters"></rt-img><rt-button theme="transparent" data-ems-id="db64beee-683b-39ce-9617-94f6b67aa997" data-mpx-id="2440952387734" data-public-id="huVWhy5k8SXH" data-type="Movie" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">Meet Marvel&#39;s First Family in The Fantastic Four: First Steps, now playing in theaters</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">Meet Marvel&#39;s First Family in The Fantastic Four: First Steps, now playing in theaters</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">0:15 </rt-badge></tile-video><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="lazy" src="https://resizing.flixster.com/tjwBmudKFkfoVN5wiJgpQPfQ9mM=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/208/351/thumb_dc59e6f6-682e-11f0-94b5-022bbbb30d69.jpg" alt="The Fantastic Four: First Steps: Spot - Love Review"></rt-img><rt-button theme="transparent" data-ems-id="db64beee-683b-39ce-9617-94f6b67aa997" data-mpx-id="2440832579995" data-public-id="OuI1XuMEBu8H" data-type="Movie" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">The Fantastic Four: First Steps: Spot - Love Review</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">The Fantastic Four: First Steps: Spot - Love Review</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">0:30 </rt-badge></tile-video><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="lazy" src="https://resizing.flixster.com/kCuTyrkJa444HyW_gTfg0zt-wxA=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/73/591/thumb_EF22AF7C-EFE5-4AAB-83F1-1154CE9997CB.jpg" alt="The Fantastic Four: First Steps: Featurette - Crafting Fantastic Four"></rt-img><rt-button theme="transparent" data-ems-id="db64beee-683b-39ce-9617-94f6b67aa997" data-mpx-id="2440691267905" data-public-id="zyEx4gF4eP51" data-type="Movie" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">The Fantastic Four: First Steps: Featurette - Crafting Fantastic Four</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">The Fantastic Four: First Steps: Featurette - Crafting Fantastic Four</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">2:27 </rt-badge></tile-video><tile-view-more aspect="landscape" background="mediaHero" slot="tile"><rt-button href="/m/the_fantastic_four_first_steps/videos" shape="pill" theme="transparent-lighttext">View more videos </rt-button></tile-view-more></carousel-slider></section></div><div class="modules-layout" tabindex="-1" data-ModulesNavigationManager="content:focusin"><div id="photos" class="dom-anchor" data-ModulesNavigationManager="domAnchor"></div><section aria-labelledby="photos-carousel-label" class="photos-carousel" data-adobe-id="photos-carousel" data-qa="section:photos-carousel"><div class="header-wrap"><div class="link-wrap"><h2 class="unset" id="photos-carousel-label"><rt-text size="1.25" context="heading">Photos</rt-text></h2><rt-button arialabel="The Fantastic Four: First Steps photos" data-qa="photos-view-all-link" href="/m/the_fantastic_four_first_steps/pictures" shape="pill" size="0.875" style="--buttonPadding: 6px 24px;--backgroundColor: var(--grayLight1);--borderColor: transparent;--letterSpacing: 1px;" theme="light">View All </rt-button></div><h3 class="unset"><rt-text context="label" size="0.75" style="--textColor: var(--grayDark4);">The Fantastic Four: First Steps </rt-text></h3></div><carousel-slider tile-width="80%,240px" skeleton="panel"><tile-photo data-PhotosCarouselManager="tilePhoto:click" slot="tile" skeleton="panel"><span class="sr-only"></span><rt-img slot="image" loading="" src="https://resizing.flixster.com/qTjkbvDHGGF4MArm23ek9NhmrBo=/fit-in/352x330/v2/https://resizing.flixster.com/WEfe-vTCqjHioD77J7f-qeoZZdY=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2FjN2M2NzA0LWNiMTctNDhjMy05N2VlLTk3YWI3N2JhZDZmMS5qcGc=,https://resizing.flixster.com/DNRCwl_allJWD22fZCnQ2W9VPmU=/fit-in/705x460/v2/https://resizing.flixster.com/WEfe-vTCqjHioD77J7f-qeoZZdY=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2FjN2M2NzA0LWNiMTctNDhjMy05N2VlLTk3YWI3N2JhZDZmMS5qcGc=" alt="The Fantastic Four: First Steps photo 1"></rt-img></tile-photo><tile-photo data-PhotosCarouselManager="tilePhoto:click" slot="tile" skeleton="panel"><span class="sr-only"></span><rt-img slot="image" loading="" src="https://resizing.flixster.com/fa9BQgkaF55O36JAf0eAPXhL_Ss=/fit-in/352x330/v2/https://resizing.flixster.com/NMakI9bPdy_rQSENNXPalE0hqnE=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzQwZDlkMDkyLTg4MmItNDEzYi04Njc4LWI0OGEyNzExMjdiOS5qcGc=,https://resizing.flixster.com/nT6_8v-aXCmJPjPnCdRI4ldAZMA=/fit-in/705x460/v2/https://resizing.flixster.com/NMakI9bPdy_rQSENNXPalE0hqnE=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzQwZDlkMDkyLTg4MmItNDEzYi04Njc4LWI0OGEyNzExMjdiOS5qcGc=" alt="The Fantastic Four: First Steps photo 2"></rt-img></tile-photo><tile-photo data-PhotosCarouselManager="tilePhoto:click" slot="tile" skeleton="panel"><span class="sr-only"></span><rt-img slot="image" loading="" src="https://resizing.flixster.com/4wA-Nucq0MUbX5TeuproT0YBjM8=/fit-in/352x330/v2/https://resizing.flixster.com/h3qjIZZh3McU1bUtnjODSl-IfYA=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2E1MWEzOTcxLWU4Y2ItNDFmZS04MGNiLWIzM2Q3MzRjZTFkMy5qcGc=,https://resizing.flixster.com/mhmTgP2FAYDZ33vLTG5E2b9d9mo=/fit-in/705x460/v2/https://resizing.flixster.com/h3qjIZZh3McU1bUtnjODSl-IfYA=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2E1MWEzOTcxLWU4Y2ItNDFmZS04MGNiLWIzM2Q3MzRjZTFkMy5qcGc=" alt="The Fantastic Four: First Steps photo 3"></rt-img></tile-photo><tile-photo data-PhotosCarouselManager="tilePhoto:click" slot="tile" skeleton="panel"><span class="sr-only"></span><rt-img slot="image" loading="" src="https://resizing.flixster.com/RY9vcciJ_oIuzUifngXUDZ79xdY=/fit-in/352x330/v2/https://resizing.flixster.com/FxaNqQIUwadQIxRy51lxUIwJeE0=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzNjMmFkYjM2LTVkNTMtNDE3NS04NjM1LTVjMmIwNzliN2QyNS5qcGc=,https://resizing.flixster.com/CYsDL5r6RiNfyJ3zEjwE9IKbnWI=/fit-in/705x460/v2/https://resizing.flixster.com/FxaNqQIUwadQIxRy51lxUIwJeE0=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzNjMmFkYjM2LTVkNTMtNDE3NS04NjM1LTVjMmIwNzliN2QyNS5qcGc=" alt="The Fantastic Four: First Steps photo 4"></rt-img></tile-photo><tile-photo data-PhotosCarouselManager="tilePhoto:click" slot="tile" skeleton="panel"><span class="sr-only"></span><rt-img slot="image" loading="" src="https://resizing.flixster.com/UGd6_E-kZvfFLpcQYg5O7coHBMk=/fit-in/352x330/v2/https://resizing.flixster.com/KR_7hcRCZbmzBhvZc1PoWsoI418=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2E0MjAzNzA0LTEzOGItNGEyMy04NTc3LTRhYTFjOGFmZWE0Ny5qcGc=,https://resizing.flixster.com/p5cVXOC6YYSAwuCbw781cGMPNs8=/fit-in/705x460/v2/https://resizing.flixster.com/KR_7hcRCZbmzBhvZc1PoWsoI418=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2E0MjAzNzA0LTEzOGItNGEyMy04NTc3LTRhYTFjOGFmZWE0Ny5qcGc=" alt="The Fantastic Four: First Steps photo 5"></rt-img></tile-photo><tile-photo data-PhotosCarouselManager="tilePhoto:click" slot="tile" skeleton="panel"><span class="sr-only"></span><rt-img slot="image" loading="lazy" src="https://resizing.flixster.com/Wfw784_pPihhDr2pwLHILl0W1LY=/fit-in/352x330/v2/https://resizing.flixster.com/yvV2l_YUKcIYbYQX1HQOBKuPR0I=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2YyZmU0Y2NiLTIxMDktNDhhMy1hNDA3LTA1NWE4NzkwY2M1My5qcGc=,https://resizing.flixster.com/LSJHJhxA-0I2RY7vXimlI-6_U3s=/fit-in/705x460/v2/https://resizing.flixster.com/yvV2l_YUKcIYbYQX1HQOBKuPR0I=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2YyZmU0Y2NiLTIxMDktNDhhMy1hNDA3LTA1NWE4NzkwY2M1My5qcGc=" alt="The Fantastic Four: First Steps photo 6"></rt-img></tile-photo><tile-photo data-PhotosCarouselManager="tilePhoto:click" slot="tile" skeleton="panel"><span class="sr-only"></span><rt-img slot="image" loading="lazy" src="https://resizing.flixster.com/rEyevvgZFFi2mdLid2lT4T4GnZg=/fit-in/352x330/v2/https://resizing.flixster.com/4nKwigwbJ03iMnCLR1ULAtCsO3Q=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzVmMGY3NDI3LWU3MDYtNDJlMC1hNTk4LWJkYjQyNTA0NTk3My5qcGc=,https://resizing.flixster.com/2esfAiJ6NQJjpe1jhK52i1nU_xA=/fit-in/705x460/v2/https://resizing.flixster.com/4nKwigwbJ03iMnCLR1ULAtCsO3Q=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzVmMGY3NDI3LWU3MDYtNDJlMC1hNTk4LWJkYjQyNTA0NTk3My5qcGc=" alt="The Fantastic Four: First Steps photo 7"></rt-img></tile-photo><tile-photo data-PhotosCarouselManager="tilePhoto:click" slot="tile" skeleton="panel"><span class="sr-only"></span><rt-img slot="image" loading="lazy" src="https://resizing.flixster.com/giAA-wPhIc6UWiOUwphKoy6ja24=/fit-in/352x330/v2/https://resizing.flixster.com/VSHaQisp5-mTkFdOOGuZSoIjvOo=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2JjMmEzOGExLTY0NTgtNDc4OS04ZGNiLWYzZmJlOThkODliOS5qcGc=,https://resizing.flixster.com/H0sOEtXy-X7obRETv-GdDs7U2r8=/fit-in/705x460/v2/https://resizing.flixster.com/VSHaQisp5-mTkFdOOGuZSoIjvOo=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2JjMmEzOGExLTY0NTgtNDc4OS04ZGNiLWYzZmJlOThkODliOS5qcGc=" alt="The Fantastic Four: First Steps photo 8"></rt-img></tile-photo><tile-photo data-PhotosCarouselManager="tilePhoto:click" slot="tile" skeleton="panel"><span class="sr-only"></span><rt-img slot="image" loading="lazy" src="https://resizing.flixster.com/vBTxZZZhcWHFF3-_Dz51uqFEkno=/fit-in/352x330/v2/https://resizing.flixster.com/2T2houQcvUwzOYX7ea74XPWk0_Y=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzFmMDMyNjVhLWMwZGUtNDdiYi04ZTQxLTk0YTBjYTFkZDFjYy5qcGc=,https://resizing.flixster.com/pQmoSin0v9SUrIuiVrOSfwsUzVM=/fit-in/705x460/v2/https://resizing.flixster.com/2T2houQcvUwzOYX7ea74XPWk0_Y=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzFmMDMyNjVhLWMwZGUtNDdiYi04ZTQxLTk0YTBjYTFkZDFjYy5qcGc=" alt="The Fantastic Four: First Steps photo 9"></rt-img></tile-photo><tile-photo data-PhotosCarouselManager="tilePhoto:click" slot="tile" skeleton="panel"><span class="sr-only"></span><rt-img slot="image" loading="lazy" src="https://resizing.flixster.com/xDDXw_gNuut7V-y4b2d0MNeGoyk=/fit-in/352x330/v2/https://resizing.flixster.com/fvWTEvDl6uXrmW6yJzxm1pjqL90=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzAzNjMwYzQ1LWY2ZTAtNGEzMi1iMTRmLWY3NTRhZTQ5Y2M2Yy5qcGc=,https://resizing.flixster.com/zwz7yir5ovsSFgwpLdn-cqsxvzU=/fit-in/705x460/v2/https://resizing.flixster.com/fvWTEvDl6uXrmW6yJzxm1pjqL90=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzAzNjMwYzQ1LWY2ZTAtNGEzMi1iMTRmLWY3NTRhZTQ5Y2M2Yy5qcGc=" alt="The Fantastic Four: First Steps photo 10"></rt-img></tile-photo><tile-view-more aspect="square,landscape" background="mediaHero" slot="tile"><rt-button href="/m/the_fantastic_four_first_steps/pictures" shape="pill" theme="transparent-lighttext" aria-label="View more The Fantastic Four: First Steps photos">View more photos </rt-button></tile-view-more></carousel-slider><photos-carousel-manager><script id="photosCarousel" type="application/json" hidden>{"title":"The Fantastic Four: First Steps","images":[{"aspectRatio":"ASPECT_RATIO_2_3","height":"3000","width":"2000","imageUrl":"https://resizing.flixster.com/DNRCwl_allJWD22fZCnQ2W9VPmU=/fit-in/705x460/v2/https://resizing.flixster.com/WEfe-vTCqjHioD77J7f-qeoZZdY=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2FjN2M2NzA0LWNiMTctNDhjMy05N2VlLTk3YWI3N2JhZDZmMS5qcGc=","imageUrlMobile":"https://resizing.flixster.com/qTjkbvDHGGF4MArm23ek9NhmrBo=/fit-in/352x330/v2/https://resizing.flixster.com/WEfe-vTCqjHioD77J7f-qeoZZdY=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2FjN2M2NzA0LWNiMTctNDhjMy05N2VlLTk3YWI3N2JhZDZmMS5qcGc=","imageLoading":""},{"aspectRatio":"ASPECT_RATIO_3_4","height":"7500","width":"5876","imageUrl":"https://resizing.flixster.com/nT6_8v-aXCmJPjPnCdRI4ldAZMA=/fit-in/705x460/v2/https://resizing.flixster.com/NMakI9bPdy_rQSENNXPalE0hqnE=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzQwZDlkMDkyLTg4MmItNDEzYi04Njc4LWI0OGEyNzExMjdiOS5qcGc=","imageUrlMobile":"https://resizing.flixster.com/fa9BQgkaF55O36JAf0eAPXhL_Ss=/fit-in/352x330/v2/https://resizing.flixster.com/NMakI9bPdy_rQSENNXPalE0hqnE=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzQwZDlkMDkyLTg4MmItNDEzYi04Njc4LWI0OGEyNzExMjdiOS5qcGc=","imageLoading":""},{"height":"5333","width":"3000","imageUrl":"https://resizing.flixster.com/mhmTgP2FAYDZ33vLTG5E2b9d9mo=/fit-in/705x460/v2/https://resizing.flixster.com/h3qjIZZh3McU1bUtnjODSl-IfYA=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2E1MWEzOTcxLWU4Y2ItNDFmZS04MGNiLWIzM2Q3MzRjZTFkMy5qcGc=","imageUrlMobile":"https://resizing.flixster.com/4wA-Nucq0MUbX5TeuproT0YBjM8=/fit-in/352x330/v2/https://resizing.flixster.com/h3qjIZZh3McU1bUtnjODSl-IfYA=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2E1MWEzOTcxLWU4Y2ItNDFmZS04MGNiLWIzM2Q3MzRjZTFkMy5qcGc=","imageLoading":""},{"height":"5333","width":"3000","imageUrl":"https://resizing.flixster.com/CYsDL5r6RiNfyJ3zEjwE9IKbnWI=/fit-in/705x460/v2/https://resizing.flixster.com/FxaNqQIUwadQIxRy51lxUIwJeE0=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzNjMmFkYjM2LTVkNTMtNDE3NS04NjM1LTVjMmIwNzliN2QyNS5qcGc=","imageUrlMobile":"https://resizing.flixster.com/RY9vcciJ_oIuzUifngXUDZ79xdY=/fit-in/352x330/v2/https://resizing.flixster.com/FxaNqQIUwadQIxRy51lxUIwJeE0=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzNjMmFkYjM2LTVkNTMtNDE3NS04NjM1LTVjMmIwNzliN2QyNS5qcGc=","imageLoading":""},{"aspectRatio":"ASPECT_RATIO_2_3","height":"3704","width":"2500","imageUrl":"https://resizing.flixster.com/p5cVXOC6YYSAwuCbw781cGMPNs8=/fit-in/705x460/v2/https://resizing.flixster.com/KR_7hcRCZbmzBhvZc1PoWsoI418=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2E0MjAzNzA0LTEzOGItNGEyMy04NTc3LTRhYTFjOGFmZWE0Ny5qcGc=","imageUrlMobile":"https://resizing.flixster.com/UGd6_E-kZvfFLpcQYg5O7coHBMk=/fit-in/352x330/v2/https://resizing.flixster.com/KR_7hcRCZbmzBhvZc1PoWsoI418=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2E0MjAzNzA0LTEzOGItNGEyMy04NTc3LTRhYTFjOGFmZWE0Ny5qcGc=","imageLoading":""},{"aspectRatio":"ASPECT_RATIO_2_3","height":"2500","width":"1688","imageUrl":"https://resizing.flixster.com/LSJHJhxA-0I2RY7vXimlI-6_U3s=/fit-in/705x460/v2/https://resizing.flixster.com/yvV2l_YUKcIYbYQX1HQOBKuPR0I=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2YyZmU0Y2NiLTIxMDktNDhhMy1hNDA3LTA1NWE4NzkwY2M1My5qcGc=","imageUrlMobile":"https://resizing.flixster.com/Wfw784_pPihhDr2pwLHILl0W1LY=/fit-in/352x330/v2/https://resizing.flixster.com/yvV2l_YUKcIYbYQX1HQOBKuPR0I=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2YyZmU0Y2NiLTIxMDktNDhhMy1hNDA3LTA1NWE4NzkwY2M1My5qcGc=","imageLoading":"lazy"},{"height":"1920","width":"1080","imageUrl":"https://resizing.flixster.com/2esfAiJ6NQJjpe1jhK52i1nU_xA=/fit-in/705x460/v2/https://resizing.flixster.com/4nKwigwbJ03iMnCLR1ULAtCsO3Q=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzVmMGY3NDI3LWU3MDYtNDJlMC1hNTk4LWJkYjQyNTA0NTk3My5qcGc=","imageUrlMobile":"https://resizing.flixster.com/rEyevvgZFFi2mdLid2lT4T4GnZg=/fit-in/352x330/v2/https://resizing.flixster.com/4nKwigwbJ03iMnCLR1ULAtCsO3Q=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzVmMGY3NDI3LWU3MDYtNDJlMC1hNTk4LWJkYjQyNTA0NTk3My5qcGc=","imageLoading":"lazy"},{"height":"1920","width":"1080","imageUrl":"https://resizing.flixster.com/H0sOEtXy-X7obRETv-GdDs7U2r8=/fit-in/705x460/v2/https://resizing.flixster.com/VSHaQisp5-mTkFdOOGuZSoIjvOo=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2JjMmEzOGExLTY0NTgtNDc4OS04ZGNiLWYzZmJlOThkODliOS5qcGc=","imageUrlMobile":"https://resizing.flixster.com/giAA-wPhIc6UWiOUwphKoy6ja24=/fit-in/352x330/v2/https://resizing.flixster.com/VSHaQisp5-mTkFdOOGuZSoIjvOo=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2JjMmEzOGExLTY0NTgtNDc4OS04ZGNiLWYzZmJlOThkODliOS5qcGc=","imageLoading":"lazy"},{"height":"1762","width":"991","imageUrl":"https://resizing.flixster.com/pQmoSin0v9SUrIuiVrOSfwsUzVM=/fit-in/705x460/v2/https://resizing.flixster.com/2T2houQcvUwzOYX7ea74XPWk0_Y=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzFmMDMyNjVhLWMwZGUtNDdiYi04ZTQxLTk0YTBjYTFkZDFjYy5qcGc=","imageUrlMobile":"https://resizing.flixster.com/vBTxZZZhcWHFF3-_Dz51uqFEkno=/fit-in/352x330/v2/https://resizing.flixster.com/2T2houQcvUwzOYX7ea74XPWk0_Y=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzFmMDMyNjVhLWMwZGUtNDdiYi04ZTQxLTk0YTBjYTFkZDFjYy5qcGc=","imageLoading":"lazy"},{"aspectRatio":"ASPECT_RATIO_2_3","height":"1609","width":"1086","imageUrl":"https://resizing.flixster.com/zwz7yir5ovsSFgwpLdn-cqsxvzU=/fit-in/705x460/v2/https://resizing.flixster.com/fvWTEvDl6uXrmW6yJzxm1pjqL90=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzAzNjMwYzQ1LWY2ZTAtNGEzMi1iMTRmLWY3NTRhZTQ5Y2M2Yy5qcGc=","imageUrlMobile":"https://resizing.flixster.com/xDDXw_gNuut7V-y4b2d0MNeGoyk=/fit-in/352x330/v2/https://resizing.flixster.com/fvWTEvDl6uXrmW6yJzxm1pjqL90=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzAzNjMwYzQ1LWY2ZTAtNGEzMi1iMTRmLWY3NTRhZTQ5Y2M2Yy5qcGc=","imageLoading":"lazy"}],"picturesPageUrl":"/m/the_fantastic_four_first_steps/pictures"} </script></photos-carousel-manager></section></div><ad-unit hidden unit-display="mobile" unit-type="mboxadtwo" show-ad-link><div slot="ad-inject" class="rectangle_ad mobile center"></div></ad-unit><ad-unit hidden unit-display="desktop" unit-type="opbannertwo"><div slot="ad-inject" class="banner-ad"></div></ad-unit><div class="modules-layout" tabindex="-1" data-ModulesNavigationManager="content:focusin"><div id="media-info" class="dom-anchor" data-ModulesNavigationManager="domAnchor"></div><section aria-labelledby="media-info-label" class="media-info" data-adobe-id="media-info" data-qa="section:media-info"><div class="header-wrap"><h2 class="unset" id="media-info-label"><rt-text context="heading" size="1.25" style="--textTransform: capitalize;" data-qa="title">Movie Info </rt-text></h2></div><div class="content-wrap"><div class="synopsis-wrap"><rt-text class="key" size="0.875" data-qa="synopsis-label">Synopsis</rt-text><rt-text data-qa="synopsis-value">Set against the vibrant backdrop of a 1960s-inspired, retro-futuristic world, Marvel Studios&#39; &quot;The Fantastic Four: First Steps&quot; introduces Marvel&#39;s First Family--Reed Richards/Mister Fantastic (Pedro Pascal), Sue Storm/Invisible Woman (Vanessa Kirby), Johnny Storm/Human Torch (Joseph Quinn) and Ben Grimm/The Thing (Ebon Moss-Bachrach) as they face their most daunting challenge yet. Forced to balance their roles as heroes with the strength of their family bond, they must defend Earth from a ravenous space god called Galactus (Ralph Ineson) and his enigmatic Herald, Silver Surfer (Julia Garner). And if Galactus&#39; plan to devour the entire planet and everyone on it weren&#39;t bad enough, it suddenly gets very personal.</rt-text></div><dl><div class="category-wrap" data-qa="item"><dt class="key"><rt-text class="key" size="0.875" data-qa="item-label">Director</rt-text></dt><dd data-qa="item-value-group"><rt-link href="/celebrity/matt-shakman" data-qa="item-value">Matt Shakman</rt-link></dd></div><div class="category-wrap" data-qa="item"><dt class="key"><rt-text class="key" size="0.875" data-qa="item-label">Producer</rt-text></dt><dd data-qa="item-value-group"><rt-link href="/celebrity/kevin_feige" data-qa="item-value">Kevin Feige</rt-link></dd></div><div class="category-wrap" data-qa="item"><dt class="key"><rt-text class="key" size="0.875" data-qa="item-label">Screenwriter</rt-text></dt><dd data-qa="item-value-group"><rt-link href="/celebrity/peter_cameron" data-qa="item-value">Peter Cameron</rt-link></dd></div><div class="category-wrap" data-qa="item"><dt class="key"><rt-text class="key" size="0.875" data-qa="item-label">Distributor</rt-text></dt><dd data-qa="item-value-group"><rt-text data-qa="item-value">Walt Disney Pictures</rt-text></dd></div><div class="category-wrap" data-qa="item"><dt class="key"><rt-text class="key" size="0.875" data-qa="item-label">Production Co</rt-text></dt><dd data-qa="item-value-group"><rt-text data-qa="item-value">Marvel Studios</rt-text></dd></div><div class="category-wrap" data-qa="item"><dt class="key"><rt-text class="key" size="0.875" data-qa="item-label">Rating</rt-text></dt><dd data-qa="item-value-group"><rt-text data-qa="item-value">PG-13 (Some Language|Action/Violence)</rt-text></dd></div><div class="category-wrap" data-qa="item"><dt class="key"><rt-text class="key" size="0.875" data-qa="item-label">Genre</rt-text></dt><dd data-qa="item-value-group"><rt-link href="/browse/movies_in_theaters/genres:action" data-qa="item-value">Action</rt-link><rt-text class="delimiter">, </rt-text><rt-link href="/browse/movies_in_theaters/genres:adventure" data-qa="item-value">Adventure</rt-link><rt-text class="delimiter">, </rt-text><rt-link href="/browse/movies_in_theaters/genres:sci_fi" data-qa="item-value">Sci-Fi</rt-link><rt-text class="delimiter">, </rt-text><rt-link href="/browse/movies_in_theaters/genres:fantasy" data-qa="item-value">Fantasy</rt-link></dd></div><div class="category-wrap" data-qa="item"><dt class="key"><rt-text class="key" size="0.875" data-qa="item-label">Original Language</rt-text></dt><dd data-qa="item-value-group"><rt-text data-qa="item-value">English</rt-text></dd></div><div class="category-wrap" data-qa="item"><dt class="key"><rt-text class="key" size="0.875" data-qa="item-label">Release Date (Theaters)</rt-text></dt><dd data-qa="item-value-group"><rt-text data-qa="item-value">Jul 25, 2025, Wide</rt-text></dd></div><div class="category-wrap" data-qa="item"><dt class="key"><rt-text class="key" size="0.875" data-qa="item-label">Box Office (Gross USA)</rt-text></dt><dd data-qa="item-value-group"><rt-text data-qa="item-value">$266.4M</rt-text></dd></div><div class="category-wrap" data-qa="item"><dt class="key"><rt-text class="key" size="0.875" data-qa="item-label">Runtime</rt-text></dt><dd data-qa="item-value-group"><rt-text data-qa="item-value">1h 54m</rt-text></dd></div><div class="category-wrap" data-qa="item"><dt class="key"><rt-text class="key" size="0.875" data-qa="item-label">Sound Mix</rt-text></dt><dd data-qa="item-value-group"><rt-text data-qa="item-value">Dolby Atmos</rt-text></dd></div><div class="category-wrap" data-qa="item"><dt class="key"><rt-text class="key" size="0.875" data-qa="item-label">Aspect Ratio</rt-text></dt><dd data-qa="item-value-group"><rt-text data-qa="item-value">Digital 2.39:1</rt-text></dd></div></dl></div></section></div></div><div id="sidebar-wrap"><div data-adobe-id="discovery-sidebar" data-DiscoverySidebarManager="sticky"><discovery-sidebar-manager><script data-json="discoverySidebarJSON" type="application/json">{"lifecycle":"IN_THEATERS","mediaType":"movie"}</script></discovery-sidebar-manager><discovery-sidebar skeleton="panel" data-DiscoverySidebarManager="sidebar"></discovery-sidebar><ad-unit data-DiscoverySidebarManager="ad:instantiated" unit-display="desktop" unit-type="topmulti" show-ad-link><div slot="ad-inject"></div></ad-unit></div></div><script id="curation-json" type="application/json">{"emsId":"db64beee-683b-39ce-9617-94f6b67aa997","hasShowtimes":true,"rtId":"900067578","type":"movie"}</script></div></div><overlay-base data-MediaAudienceReviewsManager="overlay" hidden><div slot="content"><media-review-full-audience><rt-button data-MediaAudienceReviewsManager="overlayClose:click" size="1" slot="close" theme="transparent"><rt-icon icon="close"></rt-icon></rt-button></media-review-full-audience></div></overlay-base><tool-tip data-MediaScorecardManager="tipCritics" hidden><rt-button slot="btnClose" data-MediaScorecardManager="tipCriticsClose:click" theme="transparent" size="1.5"><rt-icon icon="close" image="true"></rt-icon></rt-button><div data-MediaScorecardManager="tipCriticsContent"></div></tool-tip><tool-tip class="component" data-MediaScorecardManager="tipAudience" hidden><rt-button slot="btnClose" data-MediaScorecardManager="tipAudienceClose:click" theme="transparent" size="1.5"><rt-icon icon="close" image="true"></rt-icon></rt-button><div data-MediaScorecardManager="tipAudienceContent"></div></tool-tip><overlay-base data-MediaScorecardManager="overlay:close" hidden><div slot="content"></div></overlay-base><overlay-base data-PhotosCarouselManager="overlayBase:close" hidden><photos-carousel-overlay data-PhotosCarouselManager="photosOverlay:sliderBtnClick" slot="content"><rt-button data-PhotosCarouselManager="closeBtn:click" slot="closeBtn" theme="transparent"><rt-icon icon="close"></rt-icon></rt-button></photos-carousel-overlay></overlay-base><overlay-base data-RateAndReviewOverlayManager="overlayBase:close" hidden noclickoutside><div slot="content"></div></overlay-base><toast-notification data-RateAndReviewOverlayManager="toast" aria-live="polite" hidden><rt-icon slot="icon" icon="check-circled" image size="1"></rt-icon><rt-text slot="message" data-RateAndReviewOverlayManager="toastMessage" context="label" size="0.875">- -</rt-text><rt-button slot="close" theme="transparent"><rt-icon icon="close" image size="1"></rt-icon></rt-button></toast-notification><overlay-base data-JwPlayerManager="overlayBase:close" data-VideoPlayerOverlayManager="overlayBase:close,open" hidden><video-player-overlay class="video-overlay-wrap" data-qa="video-overlay" data-VideoPlayerOverlayManager="videoPlayerOverlay:unmute" slot="content"><rt-button data-JwPlayerManager="unmuteBtn:click" slot="unmuteBtn" theme="light"><rt-icon icon="volume-mute-fill"></rt-icon>&ensp; Tap to Unmute </rt-button><div slot="header"><button class="unset transparent" data-VideoPlayerOverlayManager="btnOverlayClose:click" data-qa="video-close-btn"><rt-icon icon="close"><span class="sr-only">Close video</span></rt-icon></button><a class="cta-btn header-cta button hide">See Details</a></div><div slot="content"></div><a slot="footer" class="cta-btn footer-cta button hide">See Details</a></video-player-overlay></overlay-base><div id="video-overlay-player" hidden></div><video-player-overlay-manager></video-player-overlay-manager><jw-player-manager data-AdsVideoSpotlightManager="jwPlayerManager:playlistItem,ready,remove" data-VideoPlayerOverlayManager="jwPlayerManager:playlistItem,pause,ready,relatedClose,relatedOpen"></jw-player-manager><ads-media-scorecard-manager></ads-media-scorecard-manager></div><back-to-top hidden></back-to-top></main><ad-unit hidden unit-display="desktop" unit-type="bottombanner"><div slot="ad-inject" class="sleaderboard_wrapper"></div></ad-unit><ads-global-skin-takeover-manager></ads-global-skin-takeover-manager><footer-manager></footer-manager><footer class="footer container" data-PagePicturesManager="footer"><mobile-app-desktop-footer env="production" hidden></mobile-app-desktop-footer><div class="footer__content-desktop-block" data-qa="footer:section"><div class="footer__content-group"><ul class="footer__links-list"><li class="footer__links-list-item"><a href="/help_desk" data-qa="footer:link-helpdesk">Help</a></li><li class="footer__links-list-item"><a href="/about" data-qa="footer:link-about">About Rotten Tomatoes</a></li><li id="footer-feedback" class="footer__links-list-item" data-qa="footer-feedback-desktop"></li></ul></div><div class="footer__content-group"><ul class="footer__links-list"><li class="footer__links-list-item"><a href="/critics/criteria" data-qa="footer:link-critic-submission">Critic Submission</a></li><li class="footer__links-list-item"><a href="/help_desk/licensing" data-qa="footer:link-licensing">Licensing</a></li><li class="footer__links-list-item"><a href="https://together.nbcuni.com/advertise/?utm_source=rotten_tomatoes&amp;utm_medium=referral&amp;utm_campaign=property_ad_pages&amp;utm_content=footer" target="_blank" rel="noopener" data-qa="footer:link-ads">Advertise With Us</a></li><li class="footer__links-list-item"><a href="//www.fandango.com/careers" target="_blank" rel="noopener" data-qa="footer:link-careers">Careers</a></li></ul></div><div class="footer__content-group footer__newsletter-block"><p class="h3 footer__content-group-title"><rt-icon icon="mail" size="1.25" style="fill:#fff"></rt-icon>&ensp;Join the Newsletter </p><p class="footer__newsletter-copy">Get the freshest reviews, news, and more delivered right to your inbox!</p><rt-button shape="pill" data-FooterManager="btnNewsLetter:click" data-qa="footer-newsletter-desktop">Join The Newsletter </rt-button><a data-FooterManager="linkNewsLetter" class="button footer__newsletter-btn hide" target="_blank" rel="noopener">Join The Newsletter </a></div><div class="footer__content-group footer__social-block" data-qa="footer:social"><p class="h3 footer__content-group-title">Follow Us</p><social-media-icons theme="light" size="20"></social-media-icons></div></div><div class="footer__content-mobile-block" data-qa="mfooter:section"><div class="footer__content-group"><div class="mobile-app-cta-wrap"><mobile-app-cta env="production" showandroid="false" showios="true" hidden></mobile-app-cta></div><p class="footer__copyright-legal" data-qa="mfooter:copyright"><rt-text size="0.75">Copyright &copy; Fandango. All rights reserved.</rt-text></p><p><rt-button shape="pill" data-FooterManager="btnNewsLetter:click" data-qa="footer-newsletter-mobile">Join The Newsletter</rt-button></p><a data-FooterManager="linkNewsLetter" class="button footer__newsletter-btn hide" target="_blank" rel="noopener">Join The Newsletter</a><ul class="footer__links-list list-inline"><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" rel="noopener" data-qa="mfooter:link-privacy-policy">Privacy Policy </a></li><li class="footer__links-list-item"><a href="/policies/terms-and-policies" data-qa="mfooter:link-terms-policies">Terms and Policies</a></li><li class="footer__links-list-item"><img data-FooterManager="iconCCPA" src="https://images.fandango.com/cms/assets/266533e0-7afb-11ed-83f2-4f600722b564--privacyoptions.svg" class="footer__ccpa-icon" loading="lazy" alt="CCPA icon" /><a href="javascript:void(0)" id="ot-sdk-btn" class="ot-sdk-show-settings mobile" data-qa="footer-cookie-settings-mobile">Cookie Settings</a></li><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/privacy/california-consumer-privacy-act" target="_blank" rel="noopener" data-qa="mfooter:link-california-notice">California Notice</a></li><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/privacy/cookies#accordionheader2" target="_blank" rel="noopener" data-qa="mfooter:link-adChoices">Ad Choices</a></li><li id="footer-feedback-mobile" class="footer__links-list-item" data-qa="footer-feedback-mobile"></li><li class="footer__links-list-item"><a href="/faq#accessibility" data-qa="mfooter:link-accessibility">Accessibility</a></li></ul></div></div><div class="footer__copyright"><ul class="footer__links-list list-inline list-inline--separator" data-qa="footer:links-list-privacy"><li class="footer__links-list-item version" data-qa="footer:version"><span>V3.1</span></li><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" rel="noopener" data-qa="footer:link-privacy-policy">Privacy Policy </a></li><li class="footer__links-list-item"><a href="/policies/terms-and-policies" data-qa="footer:link-terms-policies">Terms and Policies</a></li><li class="footer__links-list-item"><img data-FooterManager="iconCCPA" src="https://images.fandango.com/cms/assets/266533e0-7afb-11ed-83f2-4f600722b564--privacyoptions.svg" class="footer__ccpa-icon" loading="lazy" alt="CCPA icon" /><a href="javascript:void(0)" id="ot-sdk-btn" class="ot-sdk-show-settings" data-qa="footer-cookie-settings-desktop">Cookie Settings</a></li><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/privacy/california-consumer-privacy-act" target="_blank" rel="noopener" data-qa="footer:link-california-notice">California Notice</a></li><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/privacy/cookies#accordionheader2" target="_blank" rel="noopener" data-qa="footer:link-adChoices">Ad Choices</a></li><li class="footer__links-list-item"><a href="/faq#accessibility" data-qa="footer:link-accessibility">Accessibility</a></li></ul><span class="footer__copyright-legal" data-qa="footer:copyright">Copyright &copy; Fandango. A Division of <a href="https://www.nbcuniversal.com" target="_blank" rel="noopener" data-qa="footer:link-nbcuniversal">NBCUniversal</a>. All rights reserved. </span></div></footer></div><iframe-container hidden data-ArtiManager="iframeContainer:close,resize" data-iframe-src="https://arti.rottentomatoes.com?theme=iframe" theme="widget"><span slot="logo"><img src="/assets/pizza-pie/images/arti.041d204c4a4.svg" alt="Logo"></img><span>beta</span></span><rt-button aria-label="New chat" data-ArtiManager="btnNewChat:click" id="artiNewChatButton" slot="optBtns" theme="transparent" title="New chat"><rt-icon icon="new-chat" size="1.25" image></rt-icon></rt-button></iframe-container><arti-manager></arti-manager><script type="text/javascript">(function (root){ root.Fandango || (root.Fandango={}); root.Fandango.dtmData={ "webVersion": "node", "rtVersion": 3.1, "loggedInStatus": "", "customerId": "", "pageName": "trailers"}; root.RottenTomatoes || (root.RottenTomatoes={}); root.RottenTomatoes.context || (root.RottenTomatoes.context={}); root.RottenTomatoes.context.resetCookies=["AMCVS_8CF467C25245AE3F0A490D4C%40AdobeOrg", "AMCV_8CF467C25245AE3F0A490D4C%40AdobeOrg", "WRIgnore", "WRUIDAWS", "__CT_Data", "__gads", "_admrla", "_awl", "_cs_c", "_cs_id", "_cs_mk", "_cs_s", "_fbp", "_ga", "_gat_gtmTracker", "_gid", "aam_uuid", "akamai_generated_location", "auth_token", "auth_user", "auth_client", "check", "cognito", "fblo_326803741017", "fbm_326803741017", "fbsr_326803741017", "gpv_Page", "id_token", "is_auth", "loginPlatform", "mbox", "notice_behavior", "optimizelyBuckets", "optimizelyEndUserId", "optimizelyPendingLogEvents", "optimizelySegments", "s_cc", "s_dayslastvisit", "s_dayslastvisit_s", "s_invisit", "s_prevPage", "s_sq", "s_vnum", "cognito", "fbm_326803741017", "fbsr_326803741017", "id_token", "JSESSIONID", "QSI_HistorySession", "QSI_SI_8up4dWDOtjAg0hn_intercept", "_ALGOLIA", "__Host-color-scheme", "__Host-theme-options", "__host_color_scheme", "__host_theme_options", "_cb", "_cb_ls", "_cb_svref", "_chartbeat2", "_chartbeat4", "_chartbeat5", "_sp_id.47f3", "_sp_ses.47f3", "_v__chartbeat3", "adops_master_kvs", "akacd_RTReplatform", "algoliaUT", "cognito", "cl_duid", "fbsr_326803741017", "id_token", "mps_uuid", "session_id", "_admrla", "_awl", "_ga", "_gid", "aam_uuid", "cognito", "fbm_326803741017", "id_token", "_cb", "_cb_ls", "_cb_svref", "_chartbeat2", "adops_master_kvs", "cognito", "id_token", "krg_crb", "krg_uid", "mps_uuid"]; root.RottenTomatoes.criticPage={ "vanity": "nick-macwilliam", "type": "movies", "typeDisplayName": "Movie", "totalReviews": "", "criticID": "26774"}; root.RottenTomatoes.context.video={ "file": "https:\u002F\u002Flink.theplatform.com\u002Fs\u002FNGweTC\u002Fmedia\u002F4zllMWiyItlJ?formats=MPEG-DASH+widevine,M3U+appleHlsEncryption,M3U+none,MPEG-DASH+none,MPEG4,MP3", "type": "hls", "description": "Bravestone (Dwayne Johnson) fights Jurgen (Rory McCann), Ming (Awkwafina) races to help.", "image": "https:\u002F\u002Fstatcdn.fandango.com\u002FMPX\u002Fimage\u002FNBCU_Fandango\u002F986\u002F163\u002Fthumb_9ED9A836-4111-422F-A455-E5F32EBD010B.jpg", "isRedBand": false, "mediaid": "1707208771741", "mpxId": "1707208771741", "publicId": "4zllMWiyItlJ", "title": "Jumanji: The Next Level: Official Clip - The Winged Horse", "default": false, "label": "0", "duration": "3:29", "durationInSeconds": "209.919", "emsMediaType": "Movie", "emsId": "46c4dbc9-c0eb-39aa-8294-71c736ee8b45", "overviewPageUrl": "\u002Fm\u002Fjumanji_the_next_level", "videoPageUrl": "\u002Fm\u002Fjumanji_the_next_level\u002Fvideos\u002F4zllMWiyItlJ", "videoType": "CLIP", "adobeDataLayer":{ "content":{ "id": "fandango_1707208771741", "length": "209.919", "type": "vod", "player_name": "jw", "sdk_version": "web: 6.51.0", "channel": "movie", "originator": "columbia pictures, sony pictures entertainment", "name": "jumanji: the next level: official clip - the winged horse", "rating": "not adult", "stream_type": "video"}, "media_params":{ "genre": "adventure, action, comedy, fantasy", "show_type": 2}}, "comscore":{ "labelmapping": "c3=\"rottentomatoes.com\", ns_st_st=\"Rotten Tomatoes\", ns_st_pu=\"Columbia Pictures,Sony Pictures Entertainment\", ns_st_pr=\"Jumanji: The Next Level\", ns_st_sn=\"*null\", ns_st_en=\"*null\", ns_st_ge=\"Adventure,Action,Comedy,Fantasy\", ns_st_ia=\"0\", ns_st_ce=\"0\", ns_st_ddt=\"2019\", ns_st_tdt=\"2019\""}, "thumbnail": "https:\u002F\u002Fresizing.flixster.com\u002FHlKgvL8nJEw3_WZMZMURZ86FSZM=\u002F270x160\u002Fv2\u002Fhttps:\u002F\u002Fstatcdn.fandango.com\u002FMPX\u002Fimage\u002FNBCU_Fandango\u002F986\u002F163\u002Fthumb_9ED9A836-4111-422F-A455-E5F32EBD010B.jpg"}; root.RottenTomatoes.context.videoClipsJson={ "count": 14}; root.RottenTomatoes.context.review={ "mediaType": "movie", "title": "Hair Wolf", "emsId": "4d6397d6-c541-39ef-8eb0-ee8b6748f17c", "type": "all", "sort": undefined, "reviewsCount": 0, "pageInfo": undefined, "reviewerDefaultImg": "https:\u002F\u002Fimages.fandango.com\u002Fcms\u002Fassets\u002F5b6ff500-1663-11ec-ae31-05a670d2d590--rtactordefault.png", "reviewerDefaultImgWidth": "100"}; root.RottenTomatoes.context.useCursorPagination=true; root.RottenTomatoes.context.verifiedTooltip=undefined; root.RottenTomatoes.context.layout={ "header":{ "movies":{ "moviesAtHome":{ "tarsSlug": "rt-nav-movies-at-home", "linkList": [{ "header": "Fandango at Home", "slug": "fandango-at-home-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Faffiliates:fandango-at-home"},{ "header": "Peacock", "slug": "peacock-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Faffiliates:peacock"},{ "header": "Netflix", "slug": "netflix-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Faffiliates:netflix"},{ "header": "Apple TV+", "slug": "apple-tv-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Faffiliates:apple-tv-plus"},{ "header": "Prime Video", "slug": "prime-video-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Faffiliates:prime-video"},{ "header": "Most Popular Streaming movies", "slug": "most-popular-streaming-movies-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Fsort:popular"},{ "header": "Certified Fresh movies", "slug": "certified-fresh-movies-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Fcritics:certified_fresh"},{ "header": "Browse all", "slug": "browse-all-link", "url": "\u002Fbrowse\u002Fmovies_at_home"}]}}, "editorial":{ "guides":{ "posts": [{ "ID": 161109, "author": 12, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2023\u002F09\u002F600EssentialFootballMovies.png"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fguide\u002Fbest-football-movies\u002F", "status": "publish", "title": "59 Best Football Movies, Ranked by Tomatometer", "type": "guide"},{ "ID": 253470, "author": 12, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2025\u002F08\u002FBest_New_Romcoms600.jpg"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fguide\u002Fbest-new-rom-coms-romance-movies\u002F", "status": "publish", "title": "50 Best New Rom-Coms and Romance Movies", "type": "guide"}], "title": "Guides", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fcountdown\u002F"}, "hubs":{ "posts": [{ "ID": 237626, "author": 12, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2023\u002F05\u002FRT_WTW_Generic_2023_Thumbnail_600x314_021623.jpg"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Frt-hub\u002Fwhat-to-watch\u002F", "status": "publish", "title": "What to Watch: In Theaters and On Streaming", "type": "rt-hub"},{ "ID": 140214, "author": 12, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2023\u002F02\u002FRT_AwardsTour_Thumbnail_600x314.jpg"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Frt-hub\u002Fawards-tour\u002F", "status": "publish", "title": "Awards Tour", "type": "rt-hub"}], "title": "Hubs", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Frt-hubs\u002F"}, "news":{ "posts": [{ "ID": 273082, "author": 79, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2025\u002F08\u002FNew_Streaming_September_2025-Rep.jpg"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002Fnew-movies-and-shows-streaming-in-september-2025-what-to-watch-on-netflix-prime-video-hbo-max-disney-and-more\u002F", "status": "publish", "title": "New Movies and Shows Streaming in September: What to watch on Netflix, Prime Video, HBO Max, Disney+ and More", "type": "article"},{ "ID": 273326, "author": 669, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2025\u002F09\u002FConjuring_Last_Rites_Reviews-Rep.jpg"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002Fthe-conjuring-last-rites-first-reviews\u002F", "status": "publish", "title": "\u003Cem\u003EThe Conjuring: Last Rites\u003C\u002Fem\u003E First Reviews: A Frightful, Fitting Send-off", "type": "article"}], "title": "RT News", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fnews\u002F"}}, "trendingTarsSlug": "rt-nav-trending", "trending": [{ "header": "Emmy Noms", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002F2025-emmys-ballot-complete-with-tomatometer-and-popcornmeter-scores\u002F"},{ "header": "Re-Release Calendar", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002Fmovie-re-releases-calendar\u002F"},{ "header": "Renewed and Cancelled TV", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002Frenewed-and-cancelled-tv-shows-2025\u002F"},{ "header": "The Rotten Tomatoes App ", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002Fapp\u002F"}], "certifiedMedia":{ "certifiedFreshTvSeason":{ "header": null, "media":{ "url": "\u002Ftv\u002Fthe_paper_2025\u002Fs01", "name": "The Paper: Season 1", "score": 83, "posterImg": "https:\u002F\u002Fresizing.flixster.com\u002FyFijQcjPYUWUelgmiZLHgkXU7hw=\u002F206x305\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FDFkkHf5pEVX_apKtIQZcoEvI6RU=\u002Ffit-in\u002F180x240\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FtexEZJLAG-KcVpfCdkT2R1t4cmE=\u002Fems.cHJkLWVtcy1hc3NldHMvdHZzZWFzb24vYTM3OWM2MTctN2M3Ny00MjdhLTk4NDUtODE5ZWUwMWExNGRhLnBuZw=="}, "tarsSlug": "rt-nav-list-cf-picks"}, "certifiedFreshMovieInTheater":{ "header": null, "media":{ "url": "\u002Fm\u002Ftwinless", "name": "Twinless", "score": 98, "posterImg": "https:\u002F\u002Fresizing.flixster.com\u002Fj7lw2KeY9_XyfZQdqRZGku7_9C8=\u002F206x305\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FuxoeWz7uWmeYIV94_SzEV_osqe4=\u002Ffit-in\u002F180x240\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FVlylB3xT2RIYmRivMx37O3yD76Q=\u002Fems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2ZlNDQ1MGQ5LTFjN2QtNDIwNC04NWE1LTM5NGM4N2U5ZTgzYy5qcGc="}}, "certifiedFreshMovieInTheater4":{ "header": null, "media":{ "url": "\u002Fm\u002Fhamilton_2020", "name": "Hamilton", "score": 98, "posterImg": "https:\u002F\u002Fresizing.flixster.com\u002F1woquJmQfEhWCZtm7GcH0NMHsYA=\u002F206x305\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FPeAJ5ZpF5qB98ZiX6ixNDCgW2P0=\u002Ffit-in\u002F180x240\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FVmBvlTk8-z7pQvDZXTgSdj93WDE=\u002Fems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzkzY2IxZjFkLTE1NjEtNDQ4Yi05NDY3LTcxNzFmMDVhMDczNi5qcGc="}}, "certifiedFreshMovieAtHome":{ "header": null, "media":{ "url": "\u002Fm\u002Fthe_thursday_murder_club", "name": "The Thursday Murder Club", "score": 76, "posterImg": "https:\u002F\u002Fresizing.flixster.com\u002FjeeldFGcfSMgG09ey5VB7TCFiek=\u002F206x305\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002F9LXDkCzIBBNEiPURkB9t6VefF5Q=\u002Ffit-in\u002F180x240\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FrwdeR5xIiN0k7SWr6yXdnmb6zP8=\u002Fems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2EzYWFkZWJiLWE5N2MtNDc3MS1iMDRlLTk0YWVlYzI5M2UxZS5qcGc="}}, "tarsSlug": "rt-nav-list-cf-picks"}, "tvLists":{ "newTvTonight":{ "tarsSlug": "rt-hp-text-list-3", "title": "New TV Tonight", "shows": [{ "title": "Task: Season 1", "tomatometer":{ "tomatometer": 89, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Ftask\u002Fs01"},{ "title": "The Walking Dead: Daryl Dixon: Season 3", "tomatometer":{ "tomatometer": 80, "sentiment": "positive", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fthe_walking_dead_daryl_dixon\u002Fs03"},{ "title": "The Crow Girl: Season 1", "tomatometer":{ "tomatometer": 80, "sentiment": "positive", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fthe_crow_girl\u002Fs01"},{ "title": "Only Murders in the Building: Season 5", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fonly_murders_in_the_building\u002Fs05"},{ "title": "The Girlfriend: Season 1", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fthe_girlfriend\u002Fs01"},{ "title": "aka Charlie Sheen: Season 1", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Faka_charlie_sheen\u002Fs01"},{ "title": "Wizards Beyond Waverly Place: Season 2", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fwizards_beyond_waverly_place\u002Fs02"},{ "title": "Seen & Heard: the History of Black Television: Season 1", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fseen_and_heard_the_history_of_black_television\u002Fs01"},{ "title": "The Fragrant Flower Blooms With Dignity: Season 1", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fthe_fragrant_flower_blooms_with_dignity\u002Fs01"},{ "title": "Guts & Glory: Season 1", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fguts_and_glory\u002Fs01"}]}, "mostPopularTvOnRt":{ "tarsSlug": "rt-hp-text-list-2", "title": "Most Popular TV on RT", "shows": [{ "title": "The Paper: Season 1", "tomatometer":{ "tomatometer": 83, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fthe_paper_2025\u002Fs01"},{ "title": "Dexter: Resurrection: Season 1", "tomatometer":{ "tomatometer": 95, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fdexter_resurrection\u002Fs01"},{ "title": "Alien: Earth: Season 1", "tomatometer":{ "tomatometer": 95, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Falien_earth\u002Fs01"},{ "title": "Task: Season 1", "tomatometer":{ "tomatometer": 89, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Ftask\u002Fs01"},{ "title": "Wednesday: Season 2", "tomatometer":{ "tomatometer": 87, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fwednesday\u002Fs02"},{ "title": "Peacemaker: Season 2", "tomatometer":{ "tomatometer": 99, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fpeacemaker_2022\u002Fs02"},{ "title": "The Terminal List: Dark Wolf: Season 1", "tomatometer":{ "tomatometer": 73, "sentiment": "positive", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fthe_terminal_list_dark_wolf\u002Fs01"},{ "title": "Hostage: Season 1", "tomatometer":{ "tomatometer": 82, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fhostage_2025\u002Fs01"},{ "title": "Chief of War: Season 1", "tomatometer":{ "tomatometer": 93, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fchief_of_war\u002Fs01"},{ "title": "Irish Blood: Season 1", "tomatometer":{ "tomatometer": 100, "sentiment": "positive", "certified": false}, "tvPageUrl": "\u002Ftv\u002Firish_blood\u002Fs01"}]}}}, "links":{ "moviesInTheaters":{ "certifiedFresh": "\u002Fbrowse\u002Fmovies_in_theaters\u002Fcritics:certified_fresh~sort:popular", "comingSoon": "\u002Fbrowse\u002Fmovies_coming_soon\u002F", "openingThisWeek": "\u002Fbrowse\u002Fmovies_in_theaters\u002Fsort:newest", "title": "\u002Fbrowse\u002Fmovies_in_theaters", "topBoxOffice": "\u002Fbrowse\u002Fmovies_in_theaters"}, "onDvdAndStreaming":{ "all": "\u002Fbrowse\u002Fmovies_at_home\u002F", "certifiedFresh": "\u002Fbrowse\u002Fmovies_at_home\u002Fcritics:certified_fresh", "title": "\u002Fbrowse\u002Fmovies_at_home\u002F", "top": "\u002Fbrowse\u002Fmovies_at_home\u002Fsort:popular"}, "moreMovies":{ "topMovies": "\u002Fbrowse\u002Fmovies_at_home\u002Fsort:popular", "trailers": "\u002Ftrailers"}, "tvTonight": "\u002Fbrowse\u002Ftv_series_browse\u002Fsort:newest", "tvPopular": "\u002Fbrowse\u002Ftv_series_browse\u002Fsort:popular", "moreTv":{ "topTv": "\u002Fbrowse\u002Ftv_series_browse\u002Fsort:popular", "certifiedFresh": "\u002Fbrowse\u002Ftv_series_browse\u002Fcritics:fresh"}, "editorial":{ "allTimeLists": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fall-time-lists\u002F", "bingeGuide": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fbinge-guide\u002F", "comicsOnTv": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fcomics-on-tv\u002F", "countdown": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fcountdown\u002F", "fiveFavoriteFilms": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Ffive-favorite-films\u002F", "videoInterviews": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fvideo-interviews\u002F", "weekendBoxOffice": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fweekend-box-office\u002F", "weeklyKetchup": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fweekly-ketchup\u002F", "whatToWatch": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwhat-to-watch\u002F"}, "advertisingFooter": "https:\u002F\u002Ftogether.nbcuni.com\u002Fadvertise\u002F?utm_source=rotten_tomatoes&utm_medium=referral&utm_campaign=property_ad_pages&utm_content=footer", "californiaNotice": "https:\u002F\u002Fwww.nbcuniversalprivacy.com\u002Fprivacy\u002Fcalifornia-consumer-privacy-act", "careers": "\u002F\u002Fwww.fandango.com\u002Fcareers", "cookieManagement": "https:\u002F\u002Fwww.nbcuniversalprivacy.com\u002Fprivacy\u002Fcookies#accordionheader2", "fandangoAbout": "https:\u002F\u002Fwww.fandango.com\u002Fabout-us", "privacyPolicy": "https:\u002F\u002Fwww.nbcuniversalprivacy.com\u002Ffandango-privacy-policy", "termsPolicies": "\u002Fpolicies\u002Fterms-and-policies"}}; root.RottenTomatoes.thirdParty={ "chartBeat":{ "auth": "64558", "domain": "rottentomatoes.com"}, "mpx":{ "accountPid": "NGweTC", "playerPid": "y__7B0iQTi4P", "playerPidPDK6": "pdk6_y__7B0iQTi4P", "accountId": "2474312077"}, "algoliaSearch":{ "aId": "79FRDP12PN", "sId": "175588f6e5f8319b27702e4cc4013561"}, "cognito":{ "upId": "us-west-2_4L0ZX4b1U", "clientId": "7pu48v8i2n25t4vhes0edck31c"}}; root.RottenTomatoes.serviceWorker={ "isServiceWokerOn": true}; root.__RT__ || (root.__RT__={}); root.__RT__.featureFlags={ "adsCarouselHP": false, "adsCarouselHPSlug": "rt-sponsored-carousel-list-mcdonalds-hp", "adsCarouselOP": false, "adsCarouselOPSlug": "rt-sponsored-carousel-list-mcdonalds-op", "adsMockDLP": false, "adsPages": "none", "adsSponsoredOverrideOP": true, "adsSponsoredOverrideOPSlugs": "rt-sponsored-override-op-starz", "adsVideoSpotlightHP": false, "appleSigninEnabled": true, "artiEnabled": true, "authPasswordEnabled": true, "authVerboseLogs": false, "bypassCriticValidationEnabled": false, "castAndCrewEnabled": true, "cookieConsentServiceEnabled": false, "crssoEnabled": false, "editorialApiDisabled": false, "faqUpdatesEnabled": true, "legacyBridge": true, "logVerboseEnabled": false, "mobileAppAndroid": "https:\u002F\u002Fplay.google.com\u002Fstore\u002Fapps\u002Fdetails?id=com.rottentomatoes.android", "mobileAppIos": "https:\u002F\u002Fapps.apple.com\u002Fus\u002Fapp\u002Frotten-tomatoes-movies-tv\u002Fid6673916573", "mobileAppIosMeta": "app-id=6673916573, app-argument=https:\u002F\u002Fwww.rottentomatoes.com\u002F", "mobileNavEnabled": true, "oneTrustJwtApiUrl": "https:\u002F\u002Fonetrustjwt.services.fandango.com", "oneTrustJwtServiceEnabled": false, "pageJsonEnabled": false, "profilesFeaturesEnabled": false, "profilesUsernameEnabled": false, "redesignMediaHeroEnabled": true, "redesignMoreLikeThis": true, "redesignSortTable": true, "trafficAndroidEnabled": false, "trafficSafariEnabled": true, "userMigrationEnabled": true, "versantFreewheelEnabled": false, "versantMpsDomain": "app.mps.vsnt.net", "versantMpsEnabled": false, "versantOneTrustScriptBlock": "\u003C!-- OneTrust Cookies Consent Notice start for rottentomatoes.com --\u003E \u003Cscript src=\"https:\u002F\u002Fcdn.cookielaw.org\u002Fconsent\u002F01978557-1604-76a7-ad7c-18216757cf52-test\u002FotSDKStub.js\" type=\"text\u002Fjavascript\" charset=\"UTF-8\" data-domain-script=\"01978557-1604-76a7-ad7c-18216757cf52-test\" integrity=\"sha384-Exfxdyaw5OnsUlHEKlNlz7OwgVCyLlitAtJsDmSNh3LeLlCjWXos3X\u002FCMNUbQ\u002FgA\" crossorigin=\"anonymous\" \u003E\u003C\u002Fscript\u003E \u003Cscript type=\"text\u002Fjavascript\"\u003E function OptanonWrapper(){ if (OnetrustActiveGroups.includes('7')){ document.querySelector('search-results-nav-manager')?.setAlgoliaInsightUserToken?.();}} \u003C\u002Fscript\u003E \u003C!-- OneTrust Cookies Consent Notice end for rottentomatoes.com --\u003E \u003C!-- OneTrust IAB US Privacy (USP) --\u003E \u003Cscript src=\"https:\u002F\u002Fcdn.cookielaw.org\u002Fopt-out\u002FotCCPAiab.js\" id=\"privacyCookie\" type=\"text\u002Fjavascript\" charset=\"UTF-8\" ccpa-opt-out-ids=\"USP\" ccpa-opt-out-geo=\"US\" ccpa-opt-out-lspa=\"false\"\u003E\u003C\u002Fscript\u003E \u003C!-- OneTrust IAB US Privacy (USP) end --\u003E", "videoGeoFencingEnabled": true}; root.RottenTomatoes.context.adsMockDLP=false; root.RottenTomatoes.context.req={ "params":{ "vanity": "the_fantastic_four_first_steps"}, "query":{}, "route":{}, "url": "\u002Fm\u002Fthe_fantastic_four_first_steps", "secure": false, "buildVersion": undefined}; root.RottenTomatoes.context.config={}; root.BK={ "PageName": "http:\u002F\u002Fwww.rottentomatoes.com\u002Fm\u002Fthe_fantastic_four_first_steps", "SiteID": 37528, "SiteSection": "movie", "MovieId": "db64beee-683b-39ce-9617-94f6b67aa997", "MovieTitle": "The Fantastic Four: First Steps"}; root.RottenTomatoes.dtmData={ "customerId": "", "loggedInStatus": "", "rtVersion": 3.1, "webVersion": "node", "emsID": "db64beee-683b-39ce-9617-94f6b67aa997", "lifeCycleWindow": "IN_THEATERS", "pageName": "rt | movies | overview | The Fantastic Four: First Steps", "titleGenre": "Action", "titleId": "db64beee-683b-39ce-9617-94f6b67aa997", "titleName": "The Fantastic Four: First Steps", "titleType": "Movie"}; root.RottenTomatoes.context.gptSite="movie";}(this)); </script><script fetchpriority="high" src="/assets/pizza-pie/javascripts/bundles/roma/preload.18bcfff8e54.js"></script><script src="/assets/pizza-pie/javascripts/bundles/roma/vendors.a4cc402b78a.js"></script><script src="/assets/pizza-pie/javascripts/bundles/roma/default.24dc1977289.js"></script><script async data-SearchResultsNavManager="script:load" src="https://cdn.jsdelivr.net/npm/algoliasearch@4/dist/algoliasearch-lite.umd.js"></script><script src="/assets/pizza-pie/javascripts/templates/roma/searchNav.a3288ea5efe.js"></script><script src="/assets/pizza-pie/javascripts/bundles/roma/searchNav.6a836b4ca81.js"></script><script src="/assets/pizza-pie/javascripts/templates/pages/movie/index.3aa173d4c0f.js"></script><script src="/assets/pizza-pie/javascripts/bundles/pages/movie/index.3138f97b28e.js"></script><script>if (window.mps && typeof window.mps.writeFooter==='function'){ window.mps.writeFooter();} </script><script>window._satellite && _satellite.pageBottom(); </script></body></html> 1 + <!DOCTYPE html><html lang="en" dir="ltr" xmlns="http://www.w3.org/1999/xhtml" prefix="fb: http://www.facebook.com/2008/fbml og: http://opengraphprotocol.org/schema/"><head prefix="og: http://ogp.me/ns# flixstertomatoes: http://ogp.me/ns/apps/flixstertomatoes#"><script charset="UTF-8" crossorigin="anonymous" data-domain-script="7e979733-6841-4fce-9182-515fac69187f" integrity="sha384-TKdmlzVmoD70HzftTw4WtOzIBL5mNx8mXSRzEvwrWjpIJ7FZ/EuX758yMDWXtRUN" src="https://cdn.cookielaw.org/consent/7e979733-6841-4fce-9182-515fac69187f/otSDKStub.js" type="text/javascript"></script><script type="text/javascript">function OptanonWrapper(){ if (OnetrustActiveGroups.includes('7')){ document.querySelector('search-results-nav-manager')?.setAlgoliaInsightUserToken?.()}} </script><script ccpa-opt-out-ids="USP" ccpa-opt-out-geo="US" ccpa-opt-out-lspa="false" charset="UTF-8" src="https://cdn.cookielaw.org/opt-out/otCCPAiab.js" type="text/javascript"></script><script src="/assets/pizza-pie/javascripts/bundles/roma/rt-common.js?single"></script><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><meta http-equiv="x-ua-compatible" content="ie=edge"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="shortcut icon" sizes="76x76" type="image/x-icon" href="https://www.rottentomatoes.com/assets/pizza-pie/images/favicon.ico" /><title>The Fantastic Four: First Steps | Rotten Tomatoes</title><meta name="description" content="Discover reviews, ratings, and trailers for The Fantastic Four: First Steps on Rotten Tomatoes. Stay updated with critic and audience scores today!" /><meta name="twitter:card" content="summary" /><meta name="twitter:image" content="https://resizing.flixster.com/GRRDF-MY6_iS5Em5vNBg-Jd-uL0=/206x305/v2/https://resizing.flixster.com/WEfe-vTCqjHioD77J7f-qeoZZdY=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2FjN2M2NzA0LWNiMTctNDhjMy05N2VlLTk3YWI3N2JhZDZmMS5qcGc=" /><meta name="twitter:title" content="The Fantastic Four: First Steps | Rotten Tomatoes" /><meta name="twitter:text:title" content="The Fantastic Four: First Steps | Rotten Tomatoes" /><meta name="twitter:description" content="Discover reviews, ratings, and trailers for The Fantastic Four: First Steps on Rotten Tomatoes. Stay updated with critic and audience scores today!" /><meta property="og:site_name" content="Rotten Tomatoes" /><meta property="og:title" content="The Fantastic Four: First Steps | Rotten Tomatoes" /><meta property="og:description" content="Discover reviews, ratings, and trailers for The Fantastic Four: First Steps on Rotten Tomatoes. Stay updated with critic and audience scores today!" /><meta property="og:type" content="video.movie" /><meta property="og:url" content="https://www.rottentomatoes.com/m/the_fantastic_four_first_steps" /><meta property="og:image" content="https://resizing.flixster.com/GRRDF-MY6_iS5Em5vNBg-Jd-uL0=/206x305/v2/https://resizing.flixster.com/WEfe-vTCqjHioD77J7f-qeoZZdY=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2FjN2M2NzA0LWNiMTctNDhjMy05N2VlLTk3YWI3N2JhZDZmMS5qcGc=" /><meta property="og:locale" content="en_US" /><link rel="canonical" href="https://www.rottentomatoes.com/m/the_fantastic_four_first_steps" /><script type="application/ld+json">{"@context":"http://schema.org","@type":"Movie","actor":[{"@type":"Person","name":"Pedro Pascal","sameAs":"https://www.rottentomatoes.com/celebrity/pedro_pascal","image":"https://resizing.flixster.com/gHGci208eTBBe6s555qqrVvMAlg=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/494807_v9_bd.jpg"},{"@type":"Person","name":"Vanessa Kirby","sameAs":"https://www.rottentomatoes.com/celebrity/vanessa_kirby","image":"https://resizing.flixster.com/hYfFokzVjtDj7QBvMrYIJ5uAhmY=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/631337_v9_bb.jpg"},{"@type":"Person","name":"Ebon Moss-Bachrach","sameAs":"https://www.rottentomatoes.com/celebrity/ebon_moss_bachrach","image":"https://resizing.flixster.com/cEb3kX_Yn_L4trv76D6rNON5nLA=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/252022_v9_bc.jpg"},{"@type":"Person","name":"Joseph Quinn","sameAs":"https://www.rottentomatoes.com/celebrity/joseph_quinn","image":"https://resizing.flixster.com/mwGK9WCbDyzD9FKvC81OyaWTfjc=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/1102755_v9_bb.jpg"},{"@type":"Person","name":"Ralph Ineson","sameAs":"https://www.rottentomatoes.com/celebrity/ralph-ineson","image":"https://resizing.flixster.com/-UyEiZ3UKHhGzdQU4wDV10Z6wO0=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/268419_v9_bc.jpg"}],"aggregateRating":{"@type":"AggregateRating","bestRating":"100","description":"The Tomatometer rating โ€“ based on the published opinions of hundreds of film and television critics โ€“ is a trusted measurement of movie and TV programming quality for millions of moviegoers. It represents the percentage of professional critic reviews that are positive for a given film or television show.","name":"Tomatometer","ratingCount":390,"ratingValue":"87","reviewCount":390,"worstRating":"0"},"contentRating":"PG-13","dateCreated":"2025-07-25","director":[{"@type":"Person","name":"Matt Shakman","sameAs":"https://www.rottentomatoes.com/celebrity/matt-shakman","image":"https://images.fandango.com/cms/assets/b0cefeb0-b6a8-11ed-81d8-51a487a38835--poster-default-thumbnail.jpg"}],"description":"Discover reviews, ratings, and trailers for The Fantastic Four: First Steps on Rotten Tomatoes. Stay updated with critic and audience scores today!","genre":["Action","Adventure","Sci-Fi","Fantasy"],"image":"https://resizing.flixster.com/WEfe-vTCqjHioD77J7f-qeoZZdY=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2FjN2M2NzA0LWNiMTctNDhjMy05N2VlLTk3YWI3N2JhZDZmMS5qcGc=","name":"The Fantastic Four: First Steps","url":"https://www.rottentomatoes.com/m/the_fantastic_four_first_steps","video":{"@type":"VideoObject","thumbnailUrl":"https://statcdn.fandango.com/MPX/image/NBCU_Fandango/663/931/thumb_4ca6d6c5-6a62-11f0-94b5-022bbbb30d69.jpg","name":"The Fantastic Four: First Steps: 4 Us All","duration":"0:59","sourceOrganization":"MPX","uploadDate":"2025-07-26T20:55:44","description":"","contentUrl":"https://www.rottentomatoes.com/m/the_fantastic_four_first_steps/videos/BBgYzBvAIQDN"}}</script></head><body class="body no-touch js-mptd-layout" data-AdsGlobalSkinTakeoverManager="body" data-SearchResultsNavManager="body"><div id="main" class="container rt-layout__body"><main id="main_container" class="container rt-layout__content"></main></div><script type="text/javascript"></script></body></html>
+1 -1
internal/services/samples/movie_search.html
··· 1 - <!DOCTYPE html><html lang="en" dir="ltr" xmlns="http://www.w3.org/1999/xhtml" prefix="fb: http://www.facebook.com/2008/fbml og: http://opengraphprotocol.org/schema/"><head prefix="og: http://ogp.me/ns# flixstertomatoes: http://ogp.me/ns/apps/flixstertomatoes#"><script charset="UTF-8" crossorigin="anonymous" data-domain-script="7e979733-6841-4fce-9182-515fac69187f" integrity="sha384-TKdmlzVmoD70HzftTw4WtOzIBL5mNx8mXSRzEvwrWjpIJ7FZ/EuX758yMDWXtRUN" src="https://cdn.cookielaw.org/consent/7e979733-6841-4fce-9182-515fac69187f/otSDKStub.js" type="text/javascript"></script><script type="text/javascript">function OptanonWrapper(){ if (OnetrustActiveGroups.includes('7')){ document.querySelector('search-results-nav-manager')?.setAlgoliaInsightUserToken?.();}} </script><script ccpa-opt-out-ids="USP" ccpa-opt-out-geo="US" ccpa-opt-out-lspa="false" charset="UTF-8" src="https://cdn.cookielaw.org/opt-out/otCCPAiab.js" type="text/javascript"></script><script src="/assets/pizza-pie/javascripts/bundles/roma/rt-common.js?single"></script><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><meta http-equiv="x-ua-compatible" content="ie=edge"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="shortcut icon" sizes="76x76" type="image/x-icon" href="https://www.rottentomatoes.com/assets/pizza-pie/images/favicon.ico" /><title>Search Results | Rotten Tomatoes</title><meta name="description" content="Rotten Tomatoes, home of the Tomatometer, is the most trusted measurement of quality for Movies & TV. The definitive site for Reviews, Trailers, Showtimes, and Tickets"><meta property="fb:app_id" content=""><meta property="og:site_name" content="Rotten Tomatoes"><meta property="og:title" content="Search Results"><meta property="og:description" content="Rotten Tomatoes, home of the Tomatometer, is the most trusted measurement of quality for Movies & TV. The definitive site for Reviews, Trailers, Showtimes, and Tickets"><meta property="og:type" content=""><meta property="og:url" content=""><meta property="og:image" content="https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/RT_TwitterCard_2018.jpg"><meta property="og:locale" content="en_US"><meta name="twitter:card" content="summary_large_image"><meta name="twitter:image" content="https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/RT_TwitterCard_2018.jpg"><meta name="twitter:title" content="Search Results"><meta name="twitter:text:title" content="Search Results"><meta name="twitter:description" content="Rotten Tomatoes, home of the Tomatometer, is the most trusted measurement of quality for Movies & TV. The definitive site for Reviews, Trailers, Showtimes, and Tickets"><meta name="twitter:site" content="@rottentomatoes"><script>var dataLayer=dataLayer || []; dataLayer.push({ "webVersion": "node", "rtVersion": 3.1, "loggedInStatus": "", "customerId": ""}); </script><script id="mps-page-integration">window.mpscall={ "cag[score]": null, "cag[certified_fresh]": null, "cag[fresh_rotten]": null, "cag[rating]": null, "cag[release]": null, "cag[movieshow]": null, "cag[genre]": null, "cag[urlid]": null, "cat": "search|results", "field[env]": "production", "field[rtid]": null, "path": "/search", "site": "rottentomatoes-web", "title": "Search Results", "type": "results"}; var mpsopts={ 'host': 'mps.nbcuni.com', 'updatecorrelator': 1}; var mps=mps ||{}; mps._ext=mps._ext ||{}; mps._adsheld=[]; mps._queue=mps._queue ||{}; mps._queue.mpsloaded=mps._queue.mpsloaded || []; mps._queue.mpsinit=mps._queue.mpsinit || []; mps._queue.gptloaded=mps._queue.gptloaded || []; mps._queue.adload=mps._queue.adload || []; mps._queue.adclone=mps._queue.adclone || []; mps._queue.adview=mps._queue.adview || []; mps._queue.refreshads=mps._queue.refreshads || []; mps.__timer=Date.now || function (){ return +new Date}; mps.__intcode="v2"; if (typeof mps.getAd !="function") mps.getAd=function (adunit){ if (typeof adunit !="string") return false; var slotid="mps-getad-" + adunit.replace(/\W/g, ""); if (!mps._ext || !mps._ext.loaded){ mps._queue.gptloaded.push(function (){ typeof mps._gptfirst=="function" && mps._gptfirst(adunit, slotid); mps.insertAd("#" + slotid, adunit)}); mps._adsheld.push(adunit)} return '<div id="' + slotid + '" class="mps-wrapper" data-mps-fill-slot="' + adunit + '"></div>'}; </script><script src="//mps.nbcuni.com/fetch/ext/load-rottentomatoes-web.js?nowrite=2" id="mps-load"></script><link rel="manifest" href="https://www.rottentomatoes.com/assets/pizza-pie/manifest/manifest.json" /><link rel="apple-touch-icon" href="https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/apple-touch-icon-60.jpg" /><link rel="apple-touch-icon" sizes="152x152" href="https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/apple-touch-icon-152.jpg" /><link rel="apple-touch-icon" sizes="167x167" href="https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/apple-touch-icon-167.jpg" /><link rel="apple-touch-icon" sizes="180x180" href="https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/apple-touch-icon-180.jpg" /><meta name="apple-itunes-app" content="app-id=6673916573, app-argument=https://www.rottentomatoes.com/"><meta name="google-site-verification" content="VPPXtECgUUeuATBacnqnCm4ydGO99reF-xgNklSbNbc" /><meta name="msvalidate.01" content="034F16304017CA7DCF45D43850915323" /><meta name="theme-color" content="#FA320A"><meta http-equiv="x-dns-prefetch-control" content="on"><link rel="dns-prefetch" href="//www.rottentomatoes.com" /><link rel="preconnect" href="//www.rottentomatoes.com" /><link rel="stylesheet" href="/assets/pizza-pie/stylesheets/bundles/layouts/default.b6077682d41.css" /><link rel="preload" href="/assets/pizza-pie/stylesheets/bundles/pages/search.2697d48faa3.css" as="style" onload="this.onload=null;this.rel='stylesheet'" /><script>window.RottenTomatoes={}; window.RTLocals={}; window.nunjucksPrecompiled={}; window.__RT__={}; </script><script src="https://cdn.jwplayer.com/libraries/U8MHzHHR.js"></script><script src="https://sb.scorecardresearch.com/c2/plugins/streamingtag_plugin_jwplayer.js"></script><script>!function (e){ var n="https://s.go-mpulse.net/boomerang/"; if ("False"=="True") e.BOOMR_config=e.BOOMR_config ||{}, e.BOOMR_config.PageParams=e.BOOMR_config.PageParams ||{}, e.BOOMR_config.PageParams.pci=!0, n="https://s2.go-mpulse.net/boomerang/"; if (window.BOOMR_API_key="4RDDZ-2Z6GP-RRNMC-PYEUL-SK6K9", function (){ function e(){ if (!o){ var e=document.createElement("script"); e.id="boomr-scr-as", e.src=window.BOOMR.url, e.async=!0, i.parentNode.appendChild(e), o=!0}} function t(e){ o=!0; var n, t, a, r, d=document, O=window; if (window.BOOMR.snippetMethod=e ? "if" : "i", t=function (e, n){ var t=d.createElement("script"); t.id=n || "boomr-if-as", t.src=window.BOOMR.url, BOOMR_lstart=(new Date).getTime(), e=e || d.body, e.appendChild(t)}, !window.addEventListener && window.attachEvent && navigator.userAgent.match(/MSIE [67]\./)) return window.BOOMR.snippetMethod="s", void t(i.parentNode, "boomr-async"); a=document.createElement("IFRAME"), a.src="about:blank", a.title="", a.role="presentation", a.loading="eager", r=(a.frameElement || a).style, r.width=0, r.height=0, r.border=0, r.display="none", i.parentNode.appendChild(a); try{ O=a.contentWindow, d=O.document.open()} catch (_){ n=document.domain, a.src="javascript:var d=document.open();d.domain='" + n + "';void(0);", O=a.contentWindow, d=O.document.open()} if (n) d._boomrl=function (){ this.domain=n, t()}, d.write("<bo" + "dy onload='document._boomrl();'>"); else if (O._boomrl=function (){ t()}, O.addEventListener) O.addEventListener("load", O._boomrl, !1); else if (O.attachEvent) O.attachEvent("onload", O._boomrl); d.close()} function a(e){ window.BOOMR_onload=e && e.timeStamp || (new Date).getTime()} if (!window.BOOMR || !window.BOOMR.version && !window.BOOMR.snippetExecuted){ window.BOOMR=window.BOOMR ||{}, window.BOOMR.snippetStart=(new Date).getTime(), window.BOOMR.snippetExecuted=!0, window.BOOMR.snippetVersion=12, window.BOOMR.url=n + "4RDDZ-2Z6GP-RRNMC-PYEUL-SK6K9"; var i=document.currentScript || document.getElementsByTagName("script")[0], o=!1, r=document.createElement("link"); if (r.relList && "function"==typeof r.relList.supports && r.relList.supports("preload") && "as" in r) window.BOOMR.snippetMethod="p", r.href=window.BOOMR.url, r.rel="preload", r.as="script", r.addEventListener("load", e), r.addEventListener("error", function (){ t(!0)}), setTimeout(function (){ if (!o) t(!0)}, 3e3), BOOMR_lstart=(new Date).getTime(), i.parentNode.appendChild(r); else t(!1); if (window.addEventListener) window.addEventListener("load", a, !1); else if (window.attachEvent) window.attachEvent("onload", a)}}(), "".length >0) if (e && "performance" in e && e.performance && "function"==typeof e.performance.setResourceTimingBufferSize) e.performance.setResourceTimingBufferSize(); !function (){ if (BOOMR=e.BOOMR ||{}, BOOMR.plugins=BOOMR.plugins ||{}, !BOOMR.plugins.AK){ var n=""=="true" ? 1 : 0, t="", a="eyd6zaauaeceajqacqcoyaaafful3urj-f-2fe59f729-clienttons-s.akamaihd.net", i="false"=="true" ? 2 : 1, o={ "ak.v": "39", "ak.cp": "1839344", "ak.ai": parseInt("1226367", 10), "ak.ol": "0", "ak.cr": 22, "ak.ipv": 6, "ak.proto": "h2", "ak.rid": "145cc987", "ak.r": 43883, "ak.a2": n, "ak.m": "dsca", "ak.n": "essl", "ak.bpcip": "2607:ec80:1401:440::", "ak.cport": 52718, "ak.gh": "23.205.103.137", "ak.quicv": "", "ak.tlsv": "tls1.3", "ak.0rtt": "", "ak.0rtt.ed": "", "ak.csrc": "-", "ak.acc": "", "ak.t": "1757270569", "ak.ak": "hOBiQwZUYzCg5VSAfCLimQ==HoWupekmMV4uHab0PLlXY+CBQto5aTSzQcOnOHE4Fsgy79LQEzsKLX5E0tq026IpDioT3NUJGq5SyTHq8tYRa7eyXfPwcMYN1z4ggVVKuq6fT3seX33qlYTTI+DTzI2rTCjS+34g3JYwkfNX7+N1/hYIvjpvoR6Oibxnf+sOFeLJuZKhPIN8CDeCutyyRmsYodltgYR663WDRisUxoX0rAXAl+KN5/PnDB8yBn53oAusQjXUJRU+IZpKDkdXgVOvKsbAYi6TeOk2mqLbuUz0gBA3+De4ado/dIvLivpRKGbYWUqNjsbIsKV70T4/WXm8j2nFLi3EjDnHJpqD94h0kRsG9y0G6gQi9LcSYQZtwAYzU307DkzOTVa2PZP3+DhT+Rz1NuBWp3pM6oHqRDYwSe7fBOSM3WtnjLGV5lfsijc=", "ak.pv": "3", "ak.dpoabenc": "", "ak.tf": i}; if ("" !==t) o["ak.ruds"]=t; var r={ i: !1, av: function (n){ var t="http.initiator"; if (n && (!n[t] || "spa_hard"===n[t])) o["ak.feo"]=void 0 !==e.aFeoApplied ? 1 : 0, BOOMR.addVar(o)}, rv: function (){ var e=["ak.bpcip", "ak.cport", "ak.cr", "ak.csrc", "ak.gh", "ak.ipv", "ak.m", "ak.n", "ak.ol", "ak.proto", "ak.quicv", "ak.tlsv", "ak.0rtt", "ak.0rtt.ed", "ak.r", "ak.acc", "ak.t", "ak.tf"]; BOOMR.removeVar(e)}}; BOOMR.plugins.AK={ akVars: o, akDNSPreFetchDomain: a, init: function (){ if (!r.i){ var e=BOOMR.subscribe; e("before_beacon", r.av, null, null), e("onbeacon", r.rv, null, null), r.i=!0} return this}, is_complete: function (){ return !0}}}}()}(window);</script></head><body class="body no-touch js-mptd-layout" data-AdsGlobalSkinTakeoverManager="body" data-SearchResultsNavManager="body"><cookie-manager></cookie-manager><device-inspection-manager endpoint="https://www.rottentomatoes.com/napi/device/inspection"></device-inspection-manager><user-activity-manager profiles-features-enabled="false"></user-activity-manager><user-identity-manager profiles-features-enabled="false"></user-identity-manager><ad-unit-manager></ad-unit-manager><auth-initiate-manager profiles-username-enabled="false" data-ArtiManager="authInitiateManager" data-WatchlistButtonManager="authInitiateManager:createAccount"></auth-initiate-manager><auth-profile-manager data-AuthInitiateManager="authProfileManager"></auth-profile-manager><auth-validation-manager data-AuthInitiateManager="authValidation"></auth-validation-manager><overlay-base data-AuthInitiateManager="overlayBase:close" data-PagePollsIndexManager="authOverlay:close" hidden><overlay-flows data-AuthInitiateManager="overlayFlows" slot="content"><action-icon slot="close" class="auth-overlay__icon-button auth-overlay__icon-button--close" aria-label="Close" data-qa="close-overlay-btn" icon="close"></action-icon></overlay-flows></overlay-base><notification-alert data-AuthInitiateManager="authSuccess" animate hidden><rt-icon icon="check-circled"></rt-icon><span>Signed in</span></notification-alert><div id="auth-templates" data-AuthInitiateManager="authTemplates"><template slot="screens" id="account-create-username-screen"><account-create-username-screen data-qa="account-create-username-screen"><input-label slot="input-username" state="default" data-qa="username-input-label"><label slot="label" for="create-username-input">Username</label><input slot="input" id="create-username-input" type="text" placeholder="Username" data-qa="username-input" /></input-label><rt-button disabled slot="btn-continue" shape="pill" data-qa="continue-btn">Continue</rt-button><rt-text class="terms-and-policies" slot="terms-and-policies" size="0.75">By joining, you agree to the <rt-link href="/policies/terms-and-policies" target="_blank" data-qa="terms-policies-link">Terms and Policies</rt-link>and the <rt-link href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" data-qa="privacy-policy-link">Privacy Policy</rt-link>and to receive email from <rt-link href="https://www.fandango.com/about-us" target="_blank" data-qa="about-fandango-link">Fandango Media Brands</rt-link>. </rt-text></account-create-username-screen></template><template slot="screens" id="account-email-change-screen"><account-email-change-screen data-qa="account-email-change-screen" email="user@email.com"><input-label class="new-email-input" state="default" slot="new-email-input" data-qa="email-input-label"><label slot="label" for="newEmail">Enter new email</label><input slot="input" name="newEmail" type="text" placeholder="Enter new email" autocomplete="off" data-qa="email-input"></input></input-label><rt-button slot="submit-button" disabled shape="pill" data-qa="submit-btn">Submit</rt-button><rt-text class="terms-and-policies" slot="terms-and-policies" size="0.75">By joining, you agree to the <rt-link href="/policies/terms-and-policies" target="_blank" data-qa="terms-policies-link">Terms and Policies</rt-link>and <rt-link href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" data-qa="privacy-policy-link">Privacy Policy</rt-link>and to receive email from the <rt-link href="https://www.fandango.com/about-us" target="_blank" data-qa="about-fandango-link">Fandango Media Brands</rt-link>. </rt-text></account-email-change-screen></template><template slot="screens" id="account-email-change-success-screen"><account-email-change-success-screen data-qa="login-create-success-screen"><rt-text slot="message" size="1.5" style="--fontWeight: var(--franklinGothicDemi);">Email change successful</rt-text><rt-text slot="submessage">You are signed out for your security. </br>Please sign in again.</rt-text><img slot="icon" src="/assets/pizza-pie/images/icons/cognito-success.3a3d5f32fab.svg" width="111" height="105" /></account-email-change-success-screen></template><template slot="screens" id="account-password-change-screen"><account-password-change-screen data-qa="account-password-change-screen"><input-label state="default" slot="input-password-existing"><label slot="label" for="password-existing">Existing password</label><input slot="input" name="password-existing" type="password" placeholder="Enter existing password" autocomplete="off"></input></input-label><input-label state="default" slot="input-password-new"><label slot="label" for="password-new">New password</label><input slot="input" name="password-new" type="password" placeholder="Enter new password" autocomplete="off"></input></input-label><rt-button disabled shape="pill" slot="submit-button">Submit</rt-button></account-password-change-screen></template><template slot="screens" id="account-password-change-updating-screen"><login-success-screen data-qa="account-password-change-updating-screen" hidebanner><rt-text slot="status" size="1.5" style="--fontWeight: var(--franklinGothicDemi);">Updating your password... </rt-text><img slot="icon" src="/assets/pizza-pie/images/icons/cognito-success.3a3d5f32fab.svg" width="111" height="105" /></login-success-screen></template><template slot="screens" id="account-password-change-success-screen"><login-success-screen data-qa="account-password-change-success-screen" hidebanner><rt-text slot="status" size="1.5" style="--fontWeight: var(--franklinGothicDemi);">Success! </rt-text><img slot="icon" src="/assets/pizza-pie/images/icons/cognito-success.3a3d5f32fab.svg" width="111" height="105" /></login-success-screen></template><template slot="screens" id="account-verifying-email-screen"><account-verifying-email-screen data-qa="account-verifying-email-screen"><img slot="icon" src="/assets/pizza-pie/images/icons/cognito-auth-verify.e74a69e9a77.svg" alt="email" /><rt-text slot="status">Verifying your email... </rt-text><rt-button type="cta-large" href="/?authFlowScreen=loginStartScreen" slot="retryLink" size="0.875" style="--fontWeight: var(--franklinGothicMedium);" data-qa="retry-link">Retry </rt-button></account-verifying-email-screen></template><template slot="screens" id="cognito-loading"><div><loading-spinner id="cognito-auth-loading-spinner"></loading-spinner><style>#cognito-auth-loading-spinner{ font-size: 2rem; transform: translate(calc(100% - 1em), 250px); width: 50%;} </style></div></template><template slot="screens" id="login-check-email-screen"><login-check-email-screen data-qa="login-check-email-screen" email="user@email.com"><rt-text class="note-text" size="1" slot="noteText">Please open the email link from the same browser you initiated the change email process from. </rt-text><rt-text slot="gotEmailMessage" size="0.875">Didn't you get the email? </rt-text><rt-button slot="resendEmailLink" size="0.875" type="cta-large" data-qa="resend-email-link">Resend email </rt-button><rt-link context="label" slot="troubleLoginLink" size="0.875" href="/reset-client" data-qa="reset-link">Having trouble logging in?</rt-link><rt-text class="terms-and-policies" slot="termsAndPolicies" size="0.75">By continuing, you agree to the <rt-link href="/policies/terms-and-policies" target="_blank" data-qa="terms-policies-link" style="--textColor: var(--blueLink);">Terms and Policies</rt-link>and the <rt-link href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" data-qa="privacy-policy-link" style="--textColor: var(--blueLink);">Privacy Policy</rt-link>and to receive email from <rt-link href="https://www.fandango.com/about-us" target="_blank" data-qa="about-fandango-link" style="--textColor: var(--blueLink);">Fandango Media Brands</rt-link>. </rt-text></login-check-email-screen></template><template slot="screens" id="login-error-screen"><login-error-screen data-qa="login-error"><rt-text slot="header" size="1.5" context="heading" data-qa="header">Something went wrong... </rt-text><rt-text slot="description1" size="1" context="label" data-qa="description1">Please try again. </rt-text><img slot="image" src="/assets/pizza-pie/images/icons/cognito-error.c55e509a7fd.svg" /><rt-text hidden slot="description2" size="1" context="label" data-qa="description2"></rt-text><rt-link slot="ctaLink" hidden context="label" size="0.875" data-qa="retry-link">Retry</rt-link></login-error-screen></template><template slot="screens" id="login-enter-password-screen"><login-enter-password-screen data-qa="login-enter-password-screen"><rt-text slot="title" size="1.5" style="--fontWeight: var(--franklinGothicMedium);">Welcome back! </rt-text><rt-text slot="username" data-qa="user-email">username@email.com </rt-text><input-label slot="inputPassword" state="default" data-qa="password-input-label"><label slot="label" for="pass">Password</label><input slot="input" id="pass" type="password" placeholder="Password" autocomplete="off" data-qa="password-input"></input></input-label><rt-button disabled slot="continueButton" type="cta-large" data-qa="continue-btn">Continue </rt-button><rt-button slot="emailLoginButton" theme="light" shape="pill" data-qa="send-email-btn">Send email to verify </rt-button><rt-link slot="forgotPasswordLink" theme="light" data-qa="forgot-password-link">Forgot password</rt-link><rt-text class="terms-and-policies" slot="termsAndPolicies" size="0.75">By continuing, you agree to the <rt-link href="/policies/terms-and-policies" target="_blank" data-qa="terms-policies-link" style="--textColor: var(--blueLink);">Terms and Policies</rt-link>and the <rt-link href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" data-qa="privacy-policy-link" style="--textColor: var(--blueLink);">Privacy Policy</rt-link>and to receive email from <rt-link href="https://www.fandango.com/about-us" target="_blank" data-qa="about-fandango-link" style="--textColor: var(--blueLink);">Fandango Media Brands</rt-link>. </rt-text></login-enter-password-screen></template><template slot="screens" id="login-start-screen"><login-start-screen data-qa="login-start-screen"><input-label slot="inputEmail" state="default" data-qa="email-input-label"><label slot="label" for="login-email-input">Email address</label><input slot="input" autocomplete="username" id="login-email-input" placeholder="Email address" type="text" data-qa="email-input" /></input-label><rt-button disabled slot="emailLoginButton" type="cta-large" data-qa="continue-btn">Continue </rt-button><rt-button slot="googleLoginButton" shape="pill" theme="light" style="--buttonHeight: 52px; --borderRadius: 32px;" data-qa="google-login-btn" data-type="google"><div class="social-login-btn-content"><img height="16px" width="16px" src="/assets/pizza-pie/images/vendor/google/google_logo.28d9eb28faa.svg" />Continue with Google </div></rt-button><rt-button slot="appleLoginButton" shape="pill" theme="light" style="--buttonHeight: 52px; --borderRadius: 32px;" data-qa="apple-login-btn" data-type="apple"><div class="social-login-btn-content"><rt-icon size="1" icon="apple"></rt-icon>Continue with apple </div></rt-button><rt-link slot="resetLink" class="reset-link" context="label" size="0.875" href="/reset-client" data-qa="reset-link">Having trouble logging in? </rt-link><rt-text class="terms-and-policies" slot="termsAndPolicies" size="0.75">By continuing, you agree to the <rt-link href="/policies/terms-and-policies" target="_blank" data-qa="terms-policies-link" style="--textColor: var(--blueLink);">Terms and Policies</rt-link>and the <rt-link href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" data-qa="privacy-policy-link" style="--textColor: var(--blueLink);">Privacy Policy</rt-link>and to receive email from <rt-link href="https://www.fandango.com/about-us" target="_blank" data-qa="about-fandango-link" style="--textColor: var(--blueLink);">Fandango Media Brands</rt-link>. </rt-text></login-start-screen></template><template slot="screens" id="login-success-screen"><login-success-screen data-qa="login-success-screen"><rt-text slot="status" size="1.5">Login successful! </rt-text><img slot="icon" src="/assets/pizza-pie/images/icons/cognito-success.3a3d5f32fab.svg" width="111" height="105" /></login-success-screen></template><template slot="screens" id="cognito-opt-in-us"><auth-optin-screen data-qa="auth-opt-in-screen"><div slot="newsletter-text"><h2 class="cognito-optin-form__header unset">Let's keep in touch!</h2></div><img slot="image" class="image" src="https://images.fandango.com/cms/assets/97c33f00-313f-11ee-9aaf-6762c75465cf--newsletter.png" alt="Rotten Tomatoes Newsletter"><h2 slot="sub-title" class="subTitle unset">Sign up for the Rotten Tomatoes newsletter to get weekly updates on:</h2><ul slot="options"><li class="icon-item">Upcoming Movies and TV shows</li><li class="icon-item">Rotten Tomatoes Podcast</li><li class="icon-item">Media News + More</li></ul><rt-button slot="opt-in-button" data-qa="auth-opt-in-screen-opt-in-btn">Sign me up </rt-button><rt-button slot="opt-out-button" class="button--outline" data-qa="auth-opt-in-screen-opt-out-btn">No thanks </rt-button><p slot="foot-note">By clicking "Sign Me Up," you are agreeing to receive occasional emails and communications from Fandango Media (Fandango, Vudu, and Rotten Tomatoes) and consenting to Fandango's <a href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" class="optin-link" target="_blank" rel="noopener" data-qa="auth-name-screen-privacy-policy-link">Privacy Policy</a>and <a href="/policies/terms-and-policies" class="optin-link" target="_blank" rel="noopener" data-qa="auth-name-screen-privacy-policy-link">Terms and Policies</a>. Please allow 10 business days for your account to reflect your preferences. </p></auth-optin-screen></template><template slot="screens" id="cognito-opt-in-foreign"><auth-optin-screen data-qa="auth-opt-in-screen"><div slot="newsletter-text"><h2 class="cognito-optin-form__header unset">Let's keep in touch!</h2></div><img slot="image" class="image" src="https://images.fandango.com/cms/assets/97c33f00-313f-11ee-9aaf-6762c75465cf--newsletter.png" alt="Rotten Tomatoes Newsletter"><h2 slot="sub-title" class="subTitle unset">Sign up for the Rotten Tomatoes newsletter to get weekly updates on:</h2><ul slot="options"><li class="icon-item">Upcoming Movies and TV shows</li><li class="icon-item">Rotten Tomatoes Podcast</li><li class="icon-item">Media News + More</li></ul><rt-button slot="opt-in-button" data-qa="auth-opt-in-screen-opt-in-btn">Sign me up </rt-button><rt-button slot="opt-out-button" class="button--outline" data-qa="auth-opt-in-screen-opt-out-btn">No thanks </rt-button></auth-optin-screen></template><template slot="screens" id="cognito-opt-in-success"><auth-verify-screen><rt-icon icon="check-circled" slot="icon"></rt-icon><p class="h3" slot="status">OK, got it!</p></auth-verify-screen></template></div><div id="emptyPlaceholder"></div><script ASYNC src="//assets.adobedtm.com/launch-EN549327edc13e414a9beb5d61bfd9aac6.min.js"></script><div id="main" class="container rt-layout__body"><a href="#main-page-content" class="skip-link">Skip to Main Content</a><div id="header_and_leaderboard"><div id="top_leaderboard_wrapper" class="leaderboard_wrapper "><ad-unit hidden unit-display="desktop" unit-type="topbanner" adjust-height><div slot="ad-inject"></div></ad-unit><ad-unit hidden unit-display="mobile" unit-type="mbanner"><div slot="ad-inject"></div></ad-unit></div></div><rt-header-manager></rt-header-manager><rt-header aria-label="navigation bar" class="navbar" data-qa="header-nav-bar" data-AdsGlobalNavTakeoverManager="header" id="header-main" skeleton="panel"><button aria-label="Open aRTi" class="arti-mobile" data-ArtiManager="btnArti:click" slot="arti-mobile"><img alt="arti" src="/assets/pizza-pie/images/arti.041d204c4a4.svg" /></button><div slot="mobile-header-nav"><rt-button id="mobile-header-nav-btn" data-RtHeaderManager="mobileHeaderNavBtn:click" size="1.6" style="--backgroundColor: transparent; --backgroundColorHover: transparent; --buttonPadding: 0 10px 4px;">&#9776; </rt-button><mobile-header-nav id="mobile-header-nav" data-RtHeaderManager="mobileHeaderNav"><rt-img slot="logoImage" alt="Rotten Tomatoes" fetchpriority="high" src="/assets/pizza-pie/images/rt-tomato-logo.20c3bdbc97b.svg"></rt-img><div slot="menusCss"></div><div slot="menus"></div></mobile-header-nav></div><a class="logo-wrap" data-AdsGlobalNavTakeoverManager="logoLink" data-SearchResultsNavManager="rtNavLogo" href="/" id="navbar" slot="logo"><img alt="Rotten Tomatoes" data-qa="header-logo" data-AdsGlobalNavTakeoverManager="logo" src="/assets/pizza-pie/images/rtlogo.9b892cff3fd.png" fetchpriority="high" /><div class="hide"><ad-unit hidden unit-display="desktop,mobile" unit-type="logorepeat" unit-targeting="ploc=rtlogo;"><div slot="ad-inject"></div></ad-unit></div></a><search-results-nav-manager></search-results-nav-manager><search-results-nav data-adobe-id="global-nav-search" data-SearchResultsNavManager="search" slot="search" skeleton="chip"><search-results-controls data-SearchResultsNavManager="searchControls" slot="controls"><input aria-label="Search" data-AdsGlobalNavTakeoverManager="searchInput" data-SearchResultsNavManager="inputText:click,input,keydown" data-qa="search-input" placeholder="Search" slot="search-input" type="text" /><rt-button class="search-clear" data-qa="search-clear" data-AdsGlobalNavTakeoverManager="searchClearBtn" data-SearchResultsNavManager="clearBtn:click" size="0.875" slot="search-clear" theme="transparent"><rt-icon icon="close"></rt-icon></rt-button><rt-link class="search-submit" aria-label="Submit search" data-qa="search-submit" data-AdsGlobalNavTakeoverManager="searchSubmitBtn" data-SearchResultsNavManager="submitBtn:click" href="/search" size="0.875" slot="search-submit"><rt-icon icon="search"></rt-icon></rt-link><rt-button class="search-cancel" data-qa="search-cancel" data-AdsGlobalNavTakeoverManager="searchCancelBtn" data-SearchResultsNavManager="cancelBtn:click" size="0.875" slot="search-cancel" theme="transparent">Cancel </rt-button></search-results-controls><search-results aria-expanded="false" class="hide" data-SearchResultsNavManager="searchResults" slot="results"></search-results></search-results-nav><ul slot="nav-links"><li><a href="/about" data-qa="header:link-whats-tmeter" data-AdsGlobalNavTakeoverManager="text">About Rotten Tomatoes&reg; </a></li><li><a href="/critics" data-qa="header:link-critics-home" data-AdsGlobalNavTakeoverManager="text">Critics </a></li><li data-RtHeaderManager="loginLink"><ul><li><button id="masthead-show-login-btn" class="js-cognito-signin button--link" data-AuthInitiateManager="btnSignIn:click" data-qa="header:login-btn" data-AdsGlobalNavTakeoverManager="text">Login/signup </button></li></ul></li><li class="hide" data-RtHeaderManager="userItem:keydown,keyup,mouseenter" data-qa="header:user"><a class="masthead-user-link" data-RtHeaderManager="navUserlink:focus" rel="nofollow" data-qa="user-profile-link"><img data-RtHeaderManager="navUserImg" data-qa="user-profile-thumb"><p data-AdsGlobalNavTakeoverManager="text" data-RtHeaderManager="navUserFirstName" data-qa="user-profile-name"></p><rt-icon data-AdsGlobalNavTakeoverManager="text" icon="down-dir" image></rt-icon></a><rt-header-user-info class="hide" data-RtHeaderManager="userInfo:focusout,mouseleave"><a data-qa="user-stats-profile-pic" href="" rel="nofollow" slot="imageExpanded" tabindex="-1"><img src="" width="40" alt=""></a><a slot="fullName" rel="nofollow" href="" class="username" data-qa="user-stats-name"></a><a slot="wts" rel="nofollow" href="" class="wts-count-block" data-qa="user-stats-wts"><rt-icon icon="plus" data-qa="user-stats-ratings-count"></rt-icon><span class="count" data-qa="user-stats-wts-count"></span>&nbsp;Wants to See </a><a slot="rating" rel="nofollow" href="" class="rating-count-block" data-qa="user-stats-ratings"><rt-icon icon="star" data-qa="user-stats-ratings-count"></rt-icon><span class="count"></span>&nbsp;Ratings </a><a slot="profileLink" rel="nofollow" class="dropdown-link" href="" data-qa="user-stats-profile-link">Profile</a><a slot="accountLink" rel="nofollow" class="dropdown-link" href="/user/account" data-qa="user-stats-account-link">Account</a><a slot="logoutLink" class="dropdown-link" data-RtHeaderManager="logoutLink:click" href="#logout" data-qa="user-stats-logout-link">Log Out</a></rt-header-user-info></li></ul><rt-header-nav slot="nav-dropdowns"><button aria-label="Open aRTi" class="arti-desktop" data-ArtiManager="btnArti:click" slot="arti-desktop"><img alt="arti" src="/assets/pizza-pie/images/arti.041d204c4a4.svg" /></button><rt-header-nav-item slot="movies" data-qa="masthead:movies-dvds"><a class="unset" slot="link" href="/browse/movies_in_theaters/sort:popular" data-qa="masthead:movies-dvds-link" data-AdsGlobalNavTakeoverManager="text">Movies </a><rt-header-nav-item-dropdown aria-expanded="false" slot="dropdown" data-qa="movies-menu"><rt-header-nav-item-dropdown-list slot="column" data-qa="header-movies-in-theaters"><p slot="title" class="h4" data-qa="movies-in-theaters-main-link"><a class="unset" href="/browse/movies_in_theaters/sort:popular">Movies in theaters</a></p><ul slot="links"><li data-qa="in-theaters-item"><a href="/browse/movies_in_theaters/sort:newest" data-qa="opening-this-week-link">Opening This Week</a></li><li data-qa="in-theaters-item"><a href="/browse/movies_in_theaters/sort:top_box_office" data-qa="top-box-office-link">Top Box Office</a></li><li data-qa="in-theaters-item"><a href="/browse/movies_coming_soon/" data-qa="coming-soon-link">Coming Soon to Theaters</a></li><li data-qa="in-theaters-item"><a href="/browse/movies_in_theaters/critics:certified_fresh~sort:popular" data-qa="certified-fresh-link">Certified Fresh Movies</a></li></ul></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-on-dvd-streaming"><p slot="title" class="h4" data-qa="dvd-streaming-main-link"><a class="unset" href="/browse/movies_at_home">Movies at Home</a></p><ul slot="links"><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/affiliates:fandango-at-home" data-qa="fandango-at-home-link">Fandango at Home</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/affiliates:peacock" data-qa="peacock-link">Peacock</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/affiliates:netflix" data-qa="netflix-link">Netflix</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/affiliates:apple-tv-plus" data-qa="apple-tv-link">Apple TV+</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/affiliates:prime-video" data-qa="prime-video-link">Prime Video</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/sort:popular" data-qa="most-popular-streaming-movies-link">Most Popular Streaming movies</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/critics:certified_fresh" data-qa="certified-fresh-movies-link">Certified Fresh movies</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home" data-qa="browse-all-link">Browse all</a></li></ul></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-movies-more"><p slot="title" class="h4">More</p><ul slot="links"><li data-qa="what-to-watch-item"><a href="https://editorial.rottentomatoes.com/rt-hub/what-to-watch" class="what-to-watch" data-qa="what-to-watch-link">What to Watch<rt-badge>New</rt-badge></a></li></ul></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" cfp><p slot="title" class="h4">Certified fresh picks</p><ul slot="links" class="cfp-wrap" data-qa="header-certified-fresh-picks" data-curation="rt-nav-list-cf-picks"><li data-qa="cert-fresh-item"><a class="cfp-tile" href="/m/twinless" data-qa="cert-fresh-link"><tile-dynamic data-qa="tile"><rt-img alt="Twinless poster image" slot="image" src="https://resizing.flixster.com/j7lw2KeY9_XyfZQdqRZGku7_9C8=/206x305/v2/https://resizing.flixster.com/uxoeWz7uWmeYIV94_SzEV_osqe4=/fit-in/180x240/v2/https://resizing.flixster.com/VlylB3xT2RIYmRivMx37O3yD76Q=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2ZlNDQ1MGQ5LTFjN2QtNDIwNC04NWE1LTM5NGM4N2U5ZTgzYy5qcGc=" loading="lazy"></rt-img><div slot="caption" data-track="scores"><div class="score-wrap"><score-icon-critics certified sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" size="1" context="label">98%</rt-text></div><span class="p--small">Twinless</span><span class="sr-only">Link to Twinless</span></div></tile-dynamic></a></li><li data-qa="cert-fresh-item"><a class="cfp-tile" href="/m/hamilton_2020" data-qa="cert-fresh-link"><tile-dynamic data-qa="tile"><rt-img alt="Hamilton poster image" slot="image" src="https://resizing.flixster.com/1woquJmQfEhWCZtm7GcH0NMHsYA=/206x305/v2/https://resizing.flixster.com/PeAJ5ZpF5qB98ZiX6ixNDCgW2P0=/fit-in/180x240/v2/https://resizing.flixster.com/VmBvlTk8-z7pQvDZXTgSdj93WDE=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzkzY2IxZjFkLTE1NjEtNDQ4Yi05NDY3LTcxNzFmMDVhMDczNi5qcGc=" loading="lazy"></rt-img><div slot="caption" data-track="scores"><div class="score-wrap"><score-icon-critics certified sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" size="1" context="label">98%</rt-text></div><span class="p--small">Hamilton</span><span class="sr-only">Link to Hamilton</span></div></tile-dynamic></a></li><li data-qa="cert-fresh-item"><a class="cfp-tile" href="/m/the_thursday_murder_club" data-qa="cert-fresh-link"><tile-dynamic data-qa="tile"><rt-img alt="The Thursday Murder Club poster image" slot="image" src="https://resizing.flixster.com/jeeldFGcfSMgG09ey5VB7TCFiek=/206x305/v2/https://resizing.flixster.com/9LXDkCzIBBNEiPURkB9t6VefF5Q=/fit-in/180x240/v2/https://resizing.flixster.com/rwdeR5xIiN0k7SWr6yXdnmb6zP8=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2EzYWFkZWJiLWE5N2MtNDc3MS1iMDRlLTk0YWVlYzI5M2UxZS5qcGc=" loading="lazy"></rt-img><div slot="caption" data-track="scores"><div class="score-wrap"><score-icon-critics certified sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" size="1" context="label">76%</rt-text></div><span class="p--small">The Thursday Murder Club</span><span class="sr-only">Link to The Thursday Murder Club</span></div></tile-dynamic></a></li></ul></rt-header-nav-item-dropdown-list></rt-header-nav-item-dropdown></rt-header-nav-item><rt-header-nav-item slot="tv" data-qa="masthead:tv"><a class="unset" slot="link" href="/browse/tv_series_browse/sort:popular" data-qa="masthead:tv-link" data-AdsGlobalNavTakeoverManager="text">Tv shows </a><rt-header-nav-item-dropdown aria-expanded="false" slot="dropdown" data-qa="tv-menu"><rt-header-nav-item-dropdown-list slot="column" data-qa="header-tv-list1"><p slot="title" class="h4" data-curation="rt-hp-text-list-3">New TV Tonight </p><ul slot="links" class="score-list-wrap"><li data-qa="list-item"><a class="score-list-item" href="/tv/task/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">89%</rt-text></div><span>Task: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/the_walking_dead_daryl_dixon/s03" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">80%</rt-text></div><span>The Walking Dead: Daryl Dixon: Season 3 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/the_crow_girl/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">80%</rt-text></div><span>The Crow Girl: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/only_murders_in_the_building/s05" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>Only Murders in the Building: Season 5 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/the_girlfriend/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>The Girlfriend: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/aka_charlie_sheen/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>aka Charlie Sheen: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/wizards_beyond_waverly_place/s02" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>Wizards Beyond Waverly Place: Season 2 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/seen_and_heard_the_history_of_black_television/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>Seen &amp; Heard: the History of Black Television: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/the_fragrant_flower_blooms_with_dignity/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>The Fragrant Flower Blooms With Dignity: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/guts_and_glory/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>Guts &amp; Glory: Season 1 </span></a></li></ul><a class="a--short" data-qa="tv-list1-view-all-link" href="/browse/tv_series_browse/sort:newest" slot="view-all-link">View All </a></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-tv-list2"><p slot="title" class="h4" data-curation="rt-hp-text-list-2">Most Popular TV on RT </p><ul slot="links" class="score-list-wrap"><li data-qa="list-item"><a class="score-list-item" href="/tv/the_paper_2025/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">83%</rt-text></div><span>The Paper: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/dexter_resurrection/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">95%</rt-text></div><span>Dexter: Resurrection: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/alien_earth/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">95%</rt-text></div><span>Alien: Earth: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/task/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">89%</rt-text></div><span>Task: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/wednesday/s02" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">87%</rt-text></div><span>Wednesday: Season 2 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/peacemaker_2022/s02" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">99%</rt-text></div><span>Peacemaker: Season 2 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/the_terminal_list_dark_wolf/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">73%</rt-text></div><span>The Terminal List: Dark Wolf: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/hostage_2025/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">82%</rt-text></div><span>Hostage: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/chief_of_war/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">93%</rt-text></div><span>Chief of War: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/irish_blood/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">100%</rt-text></div><span>Irish Blood: Season 1 </span></a></li></ul><a class="a--short" data-qa="tv-list2-view-all-link" href="/browse/tv_series_browse/sort:popular?" slot="view-all-link">View All </a></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-tv-more"><p slot="title" class="h4">More</p><ul slot="links"><li><a href="https://editorial.rottentomatoes.com/rt-hub/what-to-watch/" class="what-to-watch" data-qa="what-to-watch-link-tv">What to Watch<rt-badge>New</rt-badge></a></li><li><a href="/browse/tv_series_browse/sort:popular" data-qa="tv-best-link"><span>Best TV Shows</span></a></li><li><a href="/browse/tv_series_browse/sort:popular" data-qa="tv-popular-link"><span>Most Popular TV</span></a></li><li><a href="/browse/tv_series_browse/affiliates:fandango-at-home" data-qa="tv-fandango-at-home-link"><span>Fandango at Home</span></a></li><li><a href="/browse/tv_series_browse/affiliates:peacock" data-qa="tv-peacock-link"><span>Peacock</span></a></li><li><a href="/browse/tv_series_browse/affiliates:paramount-plus" data-qa="tv-paramount-link"><span>Paramount+</span></a></li><li><a href="/browse/tv_series_browse/affiliates:netflix" data-qa="tv-netflix-link"><span>Netflix</span></a></li><li><a href="/browse/tv_series_browse/affiliates:prime-video" data-qa="tv-prime-video-link"><span>Prime Video</span></a></li><li><a href="/browse/tv_series_browse/affiliates:apple-tv-plus" data-qa="tv-apple-tv-plus-link"><span>Apple TV+</span></a></li></ul></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" cfp data-qa="header-certified-fresh-pick"><p slot="title" class="h4">Certified fresh pick </p><ul slot="links" class="cfp-wrap" data-curation="rt-nav-list-cf-picks"><li><a class="cfp-tile" href="/tv/the_paper_2025/s01" data-qa="cert-fresh-link"><tile-dynamic data-qa="tile"><rt-img alt="The Paper: Season 1 poster image" slot="image" src="https://resizing.flixster.com/yFijQcjPYUWUelgmiZLHgkXU7hw=/206x305/v2/https://resizing.flixster.com/DFkkHf5pEVX_apKtIQZcoEvI6RU=/fit-in/180x240/v2/https://resizing.flixster.com/texEZJLAG-KcVpfCdkT2R1t4cmE=/ems.cHJkLWVtcy1hc3NldHMvdHZzZWFzb24vYTM3OWM2MTctN2M3Ny00MjdhLTk4NDUtODE5ZWUwMWExNGRhLnBuZw==" loading="lazy"></rt-img><div slot="caption" data-track="scores"><div class="score-wrap"><score-icon-critics certified sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" size="1" context="label">83%</rt-text></div><span class="p--small">The Paper: Season 1</span><span class="sr-only">Link to The Paper: Season 1</span></div></tile-dynamic></a></li></ul></rt-header-nav-item-dropdown-list></rt-header-nav-item-dropdown></rt-header-nav-item><rt-header-nav-item slot="shop"><a class="unset" id="appLink" slot="link" href="https://editorial.rottentomatoes.com/article/app/" target="_blank" data-qa="masthead:app-link" data-AdsGlobalNavTakeoverManager="text">RT App <temporary-display slot="temporary-display" key="app" element="#appLink" event="click"><rt-badge hidden>New</rt-badge></temporary-display></a></rt-header-nav-item><rt-header-nav-item slot="news" data-qa="masthead:news"><a class="unset" slot="link" href="https://editorial.rottentomatoes.com/" data-qa="masthead:news-link" data-AdsGlobalNavTakeoverManager="text">News </a><rt-header-nav-item-dropdown aria-expanded="false" slot="dropdown" data-qa="news-menu"><rt-header-nav-item-dropdown-list slot="column" data-qa="header-news-columns"><p slot="title" class="h4">Columns</p><ul slot="links"><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/all-time-lists/" data-pageheader="All-Time Lists" data-qa="column-link">All-Time Lists </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/binge-guide/" data-pageheader="Binge Guide" data-qa="column-link">Binge Guide </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/comics-on-tv/" data-pageheader="Comics on TV" data-qa="column-link">Comics on TV </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/countdown/" data-pageheader="Countdown" data-qa="column-link">Countdown </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/five-favorite-films/" data-pageheader="Five Favorite Films" data-qa="column-link">Five Favorite Films </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/video-interviews/" data-pageheader="Video Interviews" data-qa="column-link">Video Interviews </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/weekend-box-office/" data-pageheader="Weekend Box Office" data-qa="column-link">Weekend Box Office </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/weekly-ketchup/" data-pageheader="Weekly Ketchup" data-qa="column-link">Weekly Ketchup </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/what-to-watch/" data-pageheader="What to Watch" data-qa="column-link">What to Watch </a></li></ul></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-news-guides"><p slot="title" class="h4">Guides</p><ul slot="links" class="news-wrap"><li data-qa="guides-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/guide/best-football-movies/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="59 Best Football Movies, Ranked by Tomatometer poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2023/09/600EssentialFootballMovies.png" loading="lazy"></rt-img><div slot="caption"><p>59 Best Football Movies, Ranked by Tomatometer</p><span class="sr-only">Link to 59 Best Football Movies, Ranked by Tomatometer</span></div></tile-dynamic></a></li><li data-qa="guides-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/guide/best-new-rom-coms-romance-movies/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="50 Best New Rom-Coms and Romance Movies poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2025/08/Best_New_Romcoms600.jpg" loading="lazy"></rt-img><div slot="caption"><p>50 Best New Rom-Coms and Romance Movies</p><span class="sr-only">Link to 50 Best New Rom-Coms and Romance Movies</span></div></tile-dynamic></a></li></ul><a class="a--short" data-qa="guides-view-all-link" href="https://editorial.rottentomatoes.com/countdown/" slot="view-all-link">View All </a></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-news-hubs"><p slot="title" class="h4">Hubs</p><ul slot="links" class="news-wrap"><li data-qa="hubs-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/rt-hub/what-to-watch/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="What to Watch: In Theaters and On Streaming poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2023/05/RT_WTW_Generic_2023_Thumbnail_600x314_021623.jpg" loading="lazy"></rt-img><div slot="caption"><p>What to Watch: In Theaters and On Streaming</p><span class="sr-only">Link to What to Watch: In Theaters and On Streaming</span></div></tile-dynamic></a></li><li data-qa="hubs-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/rt-hub/awards-tour/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="Awards Tour poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2023/02/RT_AwardsTour_Thumbnail_600x314.jpg" loading="lazy"></rt-img><div slot="caption"><p>Awards Tour</p><span class="sr-only">Link to Awards Tour</span></div></tile-dynamic></a></li></ul><a class="a--short" data-qa="hubs-view-all-link" href="https://editorial.rottentomatoes.com/rt-hubs/" slot="view-all-link">View All </a></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-news-rt-news"><p slot="title" class="h4">RT News</p><ul slot="links" class="news-wrap"><li data-qa="rt-news-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/article/new-movies-and-shows-streaming-in-september-2025-what-to-watch-on-netflix-prime-video-hbo-max-disney-and-more/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="New Movies and Shows Streaming in September: What to watch on Netflix, Prime Video, HBO Max, Disney+ and More poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2025/08/New_Streaming_September_2025-Rep.jpg" loading="lazy"></rt-img><div slot="caption"><p>New Movies and Shows Streaming in September: What to watch on Netflix, Prime Video, HBO Max, Disney+ and More</p><span class="sr-only">Link to New Movies and Shows Streaming in September: What to watch on Netflix, Prime Video, HBO Max, Disney+ and More</span></div></tile-dynamic></a></li><li data-qa="rt-news-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/article/the-conjuring-last-rites-first-reviews/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="<em>The Conjuring: Last Rites</em>First Reviews: A Frightful, Fitting Send-off poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2025/09/Conjuring_Last_Rites_Reviews-Rep.jpg" loading="lazy"></rt-img><div slot="caption"><p><em>The Conjuring: Last Rites</em>First Reviews: A Frightful, Fitting Send-off</p><span class="sr-only">Link to <em>The Conjuring: Last Rites</em>First Reviews: A Frightful, Fitting Send-off</span></div></tile-dynamic></a></li></ul><a class="a--short" data-qa="rt-news-view-all-link" href="https://editorial.rottentomatoes.com/news/" slot="view-all-link">View All </a></rt-header-nav-item-dropdown-list></rt-header-nav-item-dropdown></rt-header-nav-item><rt-header-nav-item slot="showtimes"><a class="unset" slot="link" href="https://www.fandango.com/movies-in-theaters?a=13036" target="_blank" rel="noopener" data-qa="masthead:tickets-showtimes-link" data-AdsGlobalNavTakeoverManager="text">Showtimes </a></rt-header-nav-item></rt-header-nav></rt-header><ads-global-nav-takeover-manager></ads-global-nav-takeover-manager><section class="trending-bar"><ad-unit hidden id="trending_bar_ad" unit-display="desktop" unit-type="trendinggraphic"><div slot="ad-inject"></div></ad-unit><div id="trending-bar-start" class="trending-list-wrap" data-qa="trending-bar"><ul class="list-inline trending-bar__list" data-curation="rt-nav-trending" data-qa="trending-bar-list"><li class="trending-bar__header">Trending on RT</li><li><a class="trending-bar__link" href="https://editorial.rottentomatoes.com/article/2025-emmys-ballot-complete-with-tomatometer-and-popcornmeter-scores/" data-qa="trending-bar-item">Emmy Noms </a></li><li><a class="trending-bar__link" href="https://editorial.rottentomatoes.com/article/movie-re-releases-calendar/" data-qa="trending-bar-item">Re-Release Calendar </a></li><li><a class="trending-bar__link" href="https://editorial.rottentomatoes.com/article/renewed-and-cancelled-tv-shows-2025/" data-qa="trending-bar-item">Renewed and Cancelled TV </a></li><li><a class="trending-bar__link" href="https://editorial.rottentomatoes.com/article/app/" data-qa="trending-bar-item">The Rotten Tomatoes App </a></li></ul><div class="trending-bar__social" data-qa="trending-bar-social-list"><social-media-icons theme="light" size="14"></social-media-icons></div></div></section><main id="main_container" class="container rt-layout__content"><div id="main-page-content"><div class="search__container layout"><section class="search__main layout__column layout__column--main"><h1 class="unset"><rt-text context="heading" size="1.625">Search Results for : "Fantastic Four"</rt-text></h1><search-page-manager searchQuery="Fantastic Four"></search-page-manager><div id="search-results" data-qa="search-results"><nav class="search__nav" slot="searchNav"><ul class="searchNav__filters"><li class="js-search-filter searchNav__filter searchNav__filter-all searchNav__filter--active" data-filter="all" tabindex="0"><span data-qa="search-filter-text">All</span></li><li class="js-search-filter searchNav__filter" data-filter="movie" tabindex="0"><span data-qa="search-filter-text">Movies (35)</span></li><li class="js-search-filter searchNav__filter" data-filter="tvSeries" tabindex="0"><span data-qa="search-filter-text">TV Shows (6)</span></li><li class="js-search-filter searchNav__filter" data-filter="celebrity" tabindex="0"><span data-qa="search-filter-text">Celebrities (2)</span></li></ul></nav><search-page-result skeleton="panel" type="movie" data-qa="search-result"><h2 class="unset" slot="title" data-qa="search-result-title"><rt-text context="heading" size="1.25">Movies </rt-text></h2><button slot="prev-btn" data-qa="paging-btn-prev">Prev</button><button slot="next-btn" data-qa="paging-btn-next">Next</button><ul slot="list"><search-page-media-row skeleton="panel" cast="Pedro Pascal,Vanessa Kirby,Ebon Moss-Bachrach" data-qa="data-row" endyear="" releaseyear="2025" startyear="" tomatometeriscertified="true" tomatometerscore="87" tomatometersentiment="POSITIVE"><a href="https://www.rottentomatoes.com/m/the_fantastic_four_first_steps" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="The Fantastic Four: First Steps" loading="lazy" src="https://resizing.flixster.com/kh9TONIlIUrcSELHRnWw0K1u3Cg=/fit-in/80x126/v2/https://resizing.flixster.com/WEfe-vTCqjHioD77J7f-qeoZZdY=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2FjN2M2NzA0LWNiMTctNDhjMy05N2VlLTk3YWI3N2JhZDZmMS5qcGc="></a><a href="https://www.rottentomatoes.com/m/the_fantastic_four_first_steps" class="unset" data-qa="info-name" slot="title">The Fantastic Four: First Steps </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Miles Teller,Michael B. Jordan,Kate Mara" data-qa="data-row" endyear="" releaseyear="2015" startyear="" tomatometeriscertified="false" tomatometerscore="9" tomatometersentiment="NEGATIVE"><a href="https://www.rottentomatoes.com/m/fantastic_four_2015" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Fantastic Four" loading="lazy" src="https://resizing.flixster.com/Dh-AxpIB7cnywbmjUyGBmgB1hFU=/fit-in/80x126/v2/https://resizing.flixster.com/EoFPwXUATnha798E0UJc9MpHV5I=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2U1ZjRiZDE1LWFmNjUtNGM3ZS1hMDIwLWQ3YjYzYTU0N2Y5NC5qcGc="></a><a href="https://www.rottentomatoes.com/m/fantastic_four_2015" class="unset" data-qa="info-name" slot="title">Fantastic Four </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Ioan Gruffudd,Jessica Alba,Chris Evans" data-qa="data-row" endyear="" releaseyear="2005" startyear="" tomatometeriscertified="false" tomatometerscore="28" tomatometersentiment="NEGATIVE"><a href="https://www.rottentomatoes.com/m/fantastic_four" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Fantastic Four" loading="lazy" src="https://resizing.flixster.com/3__TW0ho8WMbL2BtJlUY4__V7cM=/fit-in/80x126/v2/https://resizing.flixster.com/bWXub3N0V15ZoiGrLkZ5qSPmhVc=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2FlNTAzYmE0LTI2NjctNGQ2OS05MDNhLTAxYzVhM2I3NWU1OC5qcGc="></a><a href="https://www.rottentomatoes.com/m/fantastic_four" class="unset" data-qa="info-name" slot="title">Fantastic Four </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Ioan Gruffudd,Jessica Alba,Chris Evans" data-qa="data-row" endyear="" releaseyear="2007" startyear="" tomatometeriscertified="false" tomatometerscore="37" tomatometersentiment="NEGATIVE"><a href="https://www.rottentomatoes.com/m/fantastic_four_rise_of_the_silver_surfer" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Fantastic Four: Rise of the Silver Surfer" loading="lazy" src="https://resizing.flixster.com/MHurLD6EaKJSCaBEVJAQEBl51j4=/fit-in/80x126/v2/https://resizing.flixster.com/GX2x8tQN7yQMlIUbQ1k4-EnpTAw=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzc0ZTIzZWQ2LTdmNmYtNGZjNS04NmMyLTA2ZTJjMzFjMjM5Yi5qcGc="></a><a href="https://www.rottentomatoes.com/m/fantastic_four_rise_of_the_silver_surfer" class="unset" data-qa="info-name" slot="title">Fantastic Four: Rise of the Silver Surfer </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Alex Hyde-White,Jay Underwood,Rebecca Staab" data-qa="data-row" endyear="" releaseyear="1994" startyear="" tomatometeriscertified="false" tomatometerscore="33" tomatometersentiment="NEGATIVE"><a href="https://www.rottentomatoes.com/m/10005582-fantastic_four" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="The Fantastic Four" loading="lazy" src="https://resizing.flixster.com/k3h3tXzUOacsIjzSU46kNsG9jao=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p11974406_p_v10_aa.jpg"></a><a href="https://www.rottentomatoes.com/m/10005582-fantastic_four" class="unset" data-qa="info-name" slot="title">The Fantastic Four </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Carl Ciarfalio,Roger Corman,Joseph Culp" data-qa="data-row" endyear="" releaseyear="2015" startyear="" tomatometeriscertified="false" tomatometerscore="89" tomatometersentiment="POSITIVE"><a href="https://www.rottentomatoes.com/m/doomed_the_untold_story_of_roger_cormans_the_fantastic_four" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Doomed: The Untold Story of Roger Corman&#39;s the Fantastic Four" loading="lazy" src="https://resizing.flixster.com/oWBX3Tbq6dCxeofYxpduj-3eu1A=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p12021288_p_v8_ab.jpg"></a><a href="https://www.rottentomatoes.com/m/doomed_the_untold_story_of_roger_cormans_the_fantastic_four" class="unset" data-qa="info-name" slot="title">Doomed: The Untold Story of Roger Corman&#39;s the Fantastic Four </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Eddie Redmayne,Katherine Waterston,Dan Fogler" data-qa="data-row" endyear="" releaseyear="2018" startyear="" tomatometeriscertified="false" tomatometerscore="36" tomatometersentiment="NEGATIVE"><a href="https://www.rottentomatoes.com/m/fantastic_beasts_the_crimes_of_grindelwald" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Fantastic Beasts: The Crimes of Grindelwald" loading="lazy" src="https://resizing.flixster.com/jupUs-IsF4TwzhGk9zIXV5swiRQ=/fit-in/80x126/v2/https://resizing.flixster.com/47erqYRky74FKjCcwEjBZJsQO2s=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzc4M2IyOTU3LTYwMjEtNGY1Ni1hMTE4LTU3NGI5ZjVkYTM2My53ZWJw"></a><a href="https://www.rottentomatoes.com/m/fantastic_beasts_the_crimes_of_grindelwald" class="unset" data-qa="info-name" slot="title">Fantastic Beasts: The Crimes of Grindelwald </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Simon Pegg,Amara Karan,Clare Higgins" data-qa="data-row" endyear="" releaseyear="2012" startyear="" tomatometeriscertified="false" tomatometerscore="34" tomatometersentiment="NEGATIVE"><a href="https://www.rottentomatoes.com/m/a_fantastic_fear_of_everything" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="A Fantastic Fear of Everything" loading="lazy" src="https://resizing.flixster.com/GfeAPSdwANE5BnTTvY-9d_euTr8=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p9288765_p_v13_ae.jpg"></a><a href="https://www.rottentomatoes.com/m/a_fantastic_fear_of_everything" class="unset" data-qa="info-name" slot="title">A Fantastic Fear of Everything </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Mel Blanc,June Foray,Les Tremayne" data-qa="data-row" endyear="" releaseyear="1983" startyear="" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/m/daffy_ducks_movie_fantastic_island" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Daffy Duck&#39;s Movie: Fantastic Island" loading="lazy" src="https://resizing.flixster.com/fWIrhyzpAPSTAaSaVmyPBfQ5I5Y=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p6855_p_v13_ab.jpg"></a><a href="https://www.rottentomatoes.com/m/daffy_ducks_movie_fantastic_island" class="unset" data-qa="info-name" slot="title">Daffy Duck&#39;s Movie: Fantastic Island </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Christopher Lloyd,Sarah Michelle Gellar,Christopher Collet" data-qa="data-row" endyear="" releaseyear="2013" startyear="" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/m/freedom_force_2012" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Freedom Force" loading="lazy" src="https://resizing.flixster.com/YsSgG6rXnCT6eTYRepAogpgoQWI=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p10403283_p_v8_ae.jpg"></a><a href="https://www.rottentomatoes.com/m/freedom_force_2012" class="unset" data-qa="info-name" slot="title">Freedom Force </a></search-page-media-row></ul><button slot="more-btn" data-qa="search-more-btn">More Movies...</button></search-page-result><search-page-result skeleton="panel" type="tvSeries" data-qa="search-result"><h2 class="unset" slot="title" data-qa="search-result-title"><rt-text context="heading" size="1.25">TV shows </rt-text></h2><button slot="prev-btn" data-qa="paging-btn-prev">Prev</button><button slot="next-btn" data-qa="paging-btn-next">Next</button><ul slot="list"><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="2010" releaseyear="" startyear="2006" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/tv/fantastic_four_worlds_greatest_heroes" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Fantastic Four: World&#39;s Greatest Heroes" loading="lazy" src="https://resizing.flixster.com/G3rsmvYdoAMpKp7W0XhvnYtTnFA=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p384193_b_v8_af.jpg"></a><a href="https://www.rottentomatoes.com/tv/fantastic_four_worlds_greatest_heroes" class="unset" data-qa="info-name" slot="title">Fantastic Four: World&#39;s Greatest Heroes </a></search-page-media-row><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="" releaseyear="" startyear="" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/tv/fantastic_four_1994" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Fantastic Four" loading="lazy" src="https://resizing.flixster.com/-W5dxKVVhkgAIwQU6JHNp_TYNLU=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p384190_b_v8_aq.jpg"></a><a href="https://www.rottentomatoes.com/tv/fantastic_four_1994" class="unset" data-qa="info-name" slot="title">Fantastic Four </a></search-page-media-row><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="1968" releaseyear="" startyear="1967" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/tv/fantastic_four" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Fantastic Four" loading="lazy" src="https://resizing.flixster.com/0CqX0L9stEQ5Fwg1wrSCvV1L40Y=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p384191_b_v10_aa.jpg"></a><a href="https://www.rottentomatoes.com/tv/fantastic_four" class="unset" data-qa="info-name" slot="title">Fantastic Four </a></search-page-media-row><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="1978" releaseyear="" startyear="1978" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/tv/the_new_fantastic_four" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Fantastic Four" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="https://www.rottentomatoes.com/tv/the_new_fantastic_four" class="unset" data-qa="info-name" slot="title">Fantastic Four </a></search-page-media-row><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="1977" releaseyear="" startyear="1977" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/tv/the_fantastic_journey" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="The Fantastic Journey" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="https://www.rottentomatoes.com/tv/the_fantastic_journey" class="unset" data-qa="info-name" slot="title">The Fantastic Journey </a></search-page-media-row><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="2010" releaseyear="" startyear="2010" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/tv/fantasia_for_real" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Fantasia for Real" loading="lazy" src="https://resizing.flixster.com/xFR7z8b8SwD09sIUUHP9Vb6twQA=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p7967043_b_v8_aa.jpg"></a><a href="https://www.rottentomatoes.com/tv/fantasia_for_real" class="unset" data-qa="info-name" slot="title">Fantasia for Real </a></search-page-media-row></ul><button slot="more-btn" data-qa="search-more-btn">More TV Shows...</button></search-page-result><ad-unit hidden unit-display="mobile" unit-type="interscroller" no-retry><aside slot="ad-inject" class="center mobile-interscroller"></aside></ad-unit><search-page-result skeleton="panel" type="celebrity" data-qa="search-result"><h2 class="unset" slot="title" data-qa="search-result-title"><rt-text context="heading" size="1.25">Celebrities </rt-text></h2><button slot="prev-btn" data-qa="paging-btn-prev">Prev</button><button slot="next-btn" data-qa="paging-btn-next">Next</button><ul slot="list"><search-page-item-row skeleton="panel" data-qa="data-row"><a href="/celebrity/the_fantastic_four" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="The Fantastic Four" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="/celebrity/the_fantastic_four" class="unset" data-qa="info-name" slot="title"><span>The Fantastic Four</span></a></search-page-item-row><search-page-item-row skeleton="panel" data-qa="data-row"><a href="/celebrity/fantastic_our" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Fantastic รดur" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="/celebrity/fantastic_our" class="unset" data-qa="info-name" slot="title"><span>Fantastic รดur</span></a></search-page-item-row></ul><button slot="more-btn" data-qa="search-more-btn">More Celebrities...</button></search-page-result></div></section><section class="search__sidebar layout__column layout__column--sidebar"><div class="adColumn__content"><ad-unit hidden unit-display="desktop" unit-type="topmulti" show-ad-link><div slot="ad-inject"></div></ad-unit></div></section></div></div><back-to-top hidden></back-to-top></main><ad-unit hidden unit-display="desktop" unit-type="bottombanner"><div slot="ad-inject" class="sleaderboard_wrapper"></div></ad-unit><ads-global-skin-takeover-manager></ads-global-skin-takeover-manager><footer-manager></footer-manager><footer class="footer container" data-PagePicturesManager="footer"><mobile-app-desktop-footer env="production" hidden></mobile-app-desktop-footer><div class="footer__content-desktop-block" data-qa="footer:section"><div class="footer__content-group"><ul class="footer__links-list"><li class="footer__links-list-item"><a href="/help_desk" data-qa="footer:link-helpdesk">Help</a></li><li class="footer__links-list-item"><a href="/about" data-qa="footer:link-about">About Rotten Tomatoes</a></li><li id="footer-feedback" class="footer__links-list-item" data-qa="footer-feedback-desktop"></li></ul></div><div class="footer__content-group"><ul class="footer__links-list"><li class="footer__links-list-item"><a href="/critics/criteria" data-qa="footer:link-critic-submission">Critic Submission</a></li><li class="footer__links-list-item"><a href="/help_desk/licensing" data-qa="footer:link-licensing">Licensing</a></li><li class="footer__links-list-item"><a href="https://together.nbcuni.com/advertise/?utm_source=rotten_tomatoes&amp;utm_medium=referral&amp;utm_campaign=property_ad_pages&amp;utm_content=footer" target="_blank" rel="noopener" data-qa="footer:link-ads">Advertise With Us</a></li><li class="footer__links-list-item"><a href="//www.fandango.com/careers" target="_blank" rel="noopener" data-qa="footer:link-careers">Careers</a></li></ul></div><div class="footer__content-group footer__newsletter-block"><p class="h3 footer__content-group-title"><rt-icon icon="mail" size="1.25" style="fill:#fff"></rt-icon>&ensp;Join the Newsletter </p><p class="footer__newsletter-copy">Get the freshest reviews, news, and more delivered right to your inbox!</p><rt-button shape="pill" data-FooterManager="btnNewsLetter:click" data-qa="footer-newsletter-desktop">Join The Newsletter </rt-button><a data-FooterManager="linkNewsLetter" class="button footer__newsletter-btn hide" target="_blank" rel="noopener">Join The Newsletter </a></div><div class="footer__content-group footer__social-block" data-qa="footer:social"><p class="h3 footer__content-group-title">Follow Us</p><social-media-icons theme="light" size="20"></social-media-icons></div></div><div class="footer__content-mobile-block" data-qa="mfooter:section"><div class="footer__content-group"><div class="mobile-app-cta-wrap"><mobile-app-cta env="production" showandroid="false" showios="true" hidden></mobile-app-cta></div><p class="footer__copyright-legal" data-qa="mfooter:copyright"><rt-text size="0.75">Copyright &copy; Fandango. All rights reserved.</rt-text></p><p><rt-button shape="pill" data-FooterManager="btnNewsLetter:click" data-qa="footer-newsletter-mobile">Join The Newsletter</rt-button></p><a data-FooterManager="linkNewsLetter" class="button footer__newsletter-btn hide" target="_blank" rel="noopener">Join The Newsletter</a><ul class="footer__links-list list-inline"><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" rel="noopener" data-qa="mfooter:link-privacy-policy">Privacy Policy </a></li><li class="footer__links-list-item"><a href="/policies/terms-and-policies" data-qa="mfooter:link-terms-policies">Terms and Policies</a></li><li class="footer__links-list-item"><img data-FooterManager="iconCCPA" src="https://images.fandango.com/cms/assets/266533e0-7afb-11ed-83f2-4f600722b564--privacyoptions.svg" class="footer__ccpa-icon" loading="lazy" alt="CCPA icon" /><a href="javascript:void(0)" id="ot-sdk-btn" class="ot-sdk-show-settings mobile" data-qa="footer-cookie-settings-mobile">Cookie Settings</a></li><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/privacy/california-consumer-privacy-act" target="_blank" rel="noopener" data-qa="mfooter:link-california-notice">California Notice</a></li><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/privacy/cookies#accordionheader2" target="_blank" rel="noopener" data-qa="mfooter:link-adChoices">Ad Choices</a></li><li id="footer-feedback-mobile" class="footer__links-list-item" data-qa="footer-feedback-mobile"></li><li class="footer__links-list-item"><a href="/faq#accessibility" data-qa="mfooter:link-accessibility">Accessibility</a></li></ul></div></div><div class="footer__copyright"><ul class="footer__links-list list-inline list-inline--separator" data-qa="footer:links-list-privacy"><li class="footer__links-list-item version" data-qa="footer:version"><span>V3.1</span></li><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" rel="noopener" data-qa="footer:link-privacy-policy">Privacy Policy </a></li><li class="footer__links-list-item"><a href="/policies/terms-and-policies" data-qa="footer:link-terms-policies">Terms and Policies</a></li><li class="footer__links-list-item"><img data-FooterManager="iconCCPA" src="https://images.fandango.com/cms/assets/266533e0-7afb-11ed-83f2-4f600722b564--privacyoptions.svg" class="footer__ccpa-icon" loading="lazy" alt="CCPA icon" /><a href="javascript:void(0)" id="ot-sdk-btn" class="ot-sdk-show-settings" data-qa="footer-cookie-settings-desktop">Cookie Settings</a></li><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/privacy/california-consumer-privacy-act" target="_blank" rel="noopener" data-qa="footer:link-california-notice">California Notice</a></li><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/privacy/cookies#accordionheader2" target="_blank" rel="noopener" data-qa="footer:link-adChoices">Ad Choices</a></li><li class="footer__links-list-item"><a href="/faq#accessibility" data-qa="footer:link-accessibility">Accessibility</a></li></ul><span class="footer__copyright-legal" data-qa="footer:copyright">Copyright &copy; Fandango. A Division of <a href="https://www.nbcuniversal.com" target="_blank" rel="noopener" data-qa="footer:link-nbcuniversal">NBCUniversal</a>. All rights reserved. </span></div></footer></div><iframe-container hidden data-ArtiManager="iframeContainer:close,resize" data-iframe-src="https://arti.rottentomatoes.com?theme=iframe" theme="widget"><span slot="logo"><img src="/assets/pizza-pie/images/arti.041d204c4a4.svg" alt="Logo"></img><span>beta</span></span><rt-button aria-label="New chat" data-ArtiManager="btnNewChat:click" id="artiNewChatButton" slot="optBtns" theme="transparent" title="New chat"><rt-icon icon="new-chat" size="1.25" image></rt-icon></rt-button></iframe-container><arti-manager></arti-manager><script type="text/javascript">(function (root){ root.RottenTomatoes || (root.RottenTomatoes={}); root.RottenTomatoes.context || (root.RottenTomatoes.context={}); root.RottenTomatoes.context.resetCookies=["AMCVS_8CF467C25245AE3F0A490D4C%40AdobeOrg", "AMCV_8CF467C25245AE3F0A490D4C%40AdobeOrg", "WRIgnore", "WRUIDAWS", "__CT_Data", "__gads", "_admrla", "_awl", "_cs_c", "_cs_id", "_cs_mk", "_cs_s", "_fbp", "_ga", "_gat_gtmTracker", "_gid", "aam_uuid", "akamai_generated_location", "auth_token", "auth_user", "auth_client", "check", "cognito", "fblo_326803741017", "fbm_326803741017", "fbsr_326803741017", "gpv_Page", "id_token", "is_auth", "loginPlatform", "mbox", "notice_behavior", "optimizelyBuckets", "optimizelyEndUserId", "optimizelyPendingLogEvents", "optimizelySegments", "s_cc", "s_dayslastvisit", "s_dayslastvisit_s", "s_invisit", "s_prevPage", "s_sq", "s_vnum", "cognito", "fbm_326803741017", "fbsr_326803741017", "id_token", "JSESSIONID", "QSI_HistorySession", "QSI_SI_8up4dWDOtjAg0hn_intercept", "_ALGOLIA", "__Host-color-scheme", "__Host-theme-options", "__host_color_scheme", "__host_theme_options", "_cb", "_cb_ls", "_cb_svref", "_chartbeat2", "_chartbeat4", "_chartbeat5", "_sp_id.47f3", "_sp_ses.47f3", "_v__chartbeat3", "adops_master_kvs", "akacd_RTReplatform", "algoliaUT", "cognito", "cl_duid", "fbsr_326803741017", "id_token", "mps_uuid", "session_id", "_admrla", "_awl", "_ga", "_gid", "aam_uuid", "cognito", "fbm_326803741017", "id_token", "_cb", "_cb_ls", "_cb_svref", "_chartbeat2", "adops_master_kvs", "cognito", "id_token", "krg_crb", "krg_uid", "mps_uuid"]; root.Fandango || (root.Fandango={}); root.Fandango.dtmData={ "webVersion": "node", "rtVersion": 3.1, "loggedInStatus": "", "customerId": "", "pageName": "trailers"}; root.RottenTomatoes.context.video={ "file": "https:\u002F\u002Flink.theplatform.com\u002Fs\u002FNGweTC\u002Fmedia\u002F7IZc_13FDObm?formats=MPEG-DASH+widevine,M3U+appleHlsEncryption,M3U+none,MPEG-DASH+none,MPEG4,MP3", "type": "hls", "description": "Noah (Ryan Guzman) seduces high school teacher Claire (Jennifer Lopez), and the two spend a passionate night together under the covers.", "image": "https:\u002F\u002Fstatcdn.fandango.com\u002FMPX\u002Fimage\u002FNBCU_Fandango\u002F943\u002F727\u002F58042.png", "isRedBand": false, "mediaid": "625906243972", "mpxId": "625906243972", "publicId": "7IZc_13FDObm", "title": "The Boy Next Door: Official Clip - Let Me Love You", "default": false, "label": "0", "duration": "2:56", "durationInSeconds": "176.844", "emsMediaType": "Movie", "emsId": "c46b422b-8ee4-3182-b46d-09da9b195917", "overviewPageUrl": "\u002Fm\u002Fthe_boy_next_door_2015", "videoPageUrl": "\u002Fm\u002Fthe_boy_next_door_2015\u002Fvideos\u002F7IZc_13FDObm", "videoType": "CLIP", "adobeDataLayer":{ "content":{ "id": "fandango_625906243972", "length": "176.844", "type": "vod", "player_name": "jw", "sdk_version": "web: 6.51.0", "channel": "movie", "originator": "universal pictures", "name": "the boy next door: official clip - let me love you", "rating": "not adult", "stream_type": "video"}, "media_params":{ "genre": "mystery & thriller", "show_type": 2}}, "comscore":{ "labelmapping": "c3=\"rottentomatoes.com\", ns_st_st=\"Rotten Tomatoes\", ns_st_pu=\"Universal Pictures\", ns_st_pr=\"The Boy Next Door\", ns_st_sn=\"*null\", ns_st_en=\"*null\", ns_st_ge=\"Mystery & Thriller\", ns_st_ia=\"0\", ns_st_ce=\"0\", ns_st_ddt=\"2015\", ns_st_tdt=\"2015\""}, "thumbnail": "https:\u002F\u002Fresizing.flixster.com\u002Fk4o6Idbg7006vQ2FmLxnQrCxeTA=\u002F270x160\u002Fv2\u002Fhttps:\u002F\u002Fstatcdn.fandango.com\u002FMPX\u002Fimage\u002FNBCU_Fandango\u002F943\u002F727\u002F58042.png"}; root.RottenTomatoes.context.videoClipsJson={ "count": 11}; root.RottenTomatoes.criticPage={ "vanity": "dave-walker", "type": "movies", "typeDisplayName": "Movie", "totalReviews": "", "criticID": "15606"}; root.RottenTomatoes.context.req={ "params":{ "vanity": "many_adventures_of_winnie_the_pooh", "reviewType": undefined}, "query":{}, "route":{}, "url": "\u002Fm\u002Fmany_adventures_of_winnie_the_pooh\u002Freviews", "secure": false, "buildVersion": undefined}; root.RottenTomatoes.context.config={}; root.RottenTomatoes.context.review={ "mediaType": "movie", "title": "The Many Adventures of Winnie the Pooh", "emsId": "3ca166f3-fb40-3899-8faa-b11f954924d7", "type": "all", "sort": undefined, "reviewsCount": 0, "pageInfo": undefined, "reviewerDefaultImg": "https:\u002F\u002Fimages.fandango.com\u002Fcms\u002Fassets\u002F5b6ff500-1663-11ec-ae31-05a670d2d590--rtactordefault.png", "reviewerDefaultImgWidth": "100"}; root.RottenTomatoes.context.useCursorPagination=true; root.RottenTomatoes.context.verifiedTooltip=undefined; root.RottenTomatoes.context.layout={ "header":{ "movies":{ "moviesAtHome":{ "tarsSlug": "rt-nav-movies-at-home", "linkList": [{ "header": "Fandango at Home", "slug": "fandango-at-home-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Faffiliates:fandango-at-home"},{ "header": "Peacock", "slug": "peacock-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Faffiliates:peacock"},{ "header": "Netflix", "slug": "netflix-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Faffiliates:netflix"},{ "header": "Apple TV+", "slug": "apple-tv-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Faffiliates:apple-tv-plus"},{ "header": "Prime Video", "slug": "prime-video-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Faffiliates:prime-video"},{ "header": "Most Popular Streaming movies", "slug": "most-popular-streaming-movies-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Fsort:popular"},{ "header": "Certified Fresh movies", "slug": "certified-fresh-movies-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Fcritics:certified_fresh"},{ "header": "Browse all", "slug": "browse-all-link", "url": "\u002Fbrowse\u002Fmovies_at_home"}]}}, "editorial":{ "guides":{ "posts": [{ "ID": 161109, "author": 12, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2023\u002F09\u002F600EssentialFootballMovies.png"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fguide\u002Fbest-football-movies\u002F", "status": "publish", "title": "59 Best Football Movies, Ranked by Tomatometer", "type": "guide"},{ "ID": 253470, "author": 12, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2025\u002F08\u002FBest_New_Romcoms600.jpg"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fguide\u002Fbest-new-rom-coms-romance-movies\u002F", "status": "publish", "title": "50 Best New Rom-Coms and Romance Movies", "type": "guide"}], "title": "Guides", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fcountdown\u002F"}, "hubs":{ "posts": [{ "ID": 237626, "author": 12, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2023\u002F05\u002FRT_WTW_Generic_2023_Thumbnail_600x314_021623.jpg"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Frt-hub\u002Fwhat-to-watch\u002F", "status": "publish", "title": "What to Watch: In Theaters and On Streaming", "type": "rt-hub"},{ "ID": 140214, "author": 12, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2023\u002F02\u002FRT_AwardsTour_Thumbnail_600x314.jpg"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Frt-hub\u002Fawards-tour\u002F", "status": "publish", "title": "Awards Tour", "type": "rt-hub"}], "title": "Hubs", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Frt-hubs\u002F"}, "news":{ "posts": [{ "ID": 273082, "author": 79, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2025\u002F08\u002FNew_Streaming_September_2025-Rep.jpg"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002Fnew-movies-and-shows-streaming-in-september-2025-what-to-watch-on-netflix-prime-video-hbo-max-disney-and-more\u002F", "status": "publish", "title": "New Movies and Shows Streaming in September: What to watch on Netflix, Prime Video, HBO Max, Disney+ and More", "type": "article"},{ "ID": 273326, "author": 669, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2025\u002F09\u002FConjuring_Last_Rites_Reviews-Rep.jpg"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002Fthe-conjuring-last-rites-first-reviews\u002F", "status": "publish", "title": "\u003Cem\u003EThe Conjuring: Last Rites\u003C\u002Fem\u003E First Reviews: A Frightful, Fitting Send-off", "type": "article"}], "title": "RT News", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fnews\u002F"}}, "trendingTarsSlug": "rt-nav-trending", "trending": [{ "header": "Emmy Noms", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002F2025-emmys-ballot-complete-with-tomatometer-and-popcornmeter-scores\u002F"},{ "header": "Re-Release Calendar", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002Fmovie-re-releases-calendar\u002F"},{ "header": "Renewed and Cancelled TV", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002Frenewed-and-cancelled-tv-shows-2025\u002F"},{ "header": "The Rotten Tomatoes App ", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002Fapp\u002F"}], "certifiedMedia":{ "certifiedFreshTvSeason":{ "header": null, "media":{ "url": "\u002Ftv\u002Fthe_paper_2025\u002Fs01", "name": "The Paper: Season 1", "score": 83, "posterImg": "https:\u002F\u002Fresizing.flixster.com\u002FyFijQcjPYUWUelgmiZLHgkXU7hw=\u002F206x305\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FDFkkHf5pEVX_apKtIQZcoEvI6RU=\u002Ffit-in\u002F180x240\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FtexEZJLAG-KcVpfCdkT2R1t4cmE=\u002Fems.cHJkLWVtcy1hc3NldHMvdHZzZWFzb24vYTM3OWM2MTctN2M3Ny00MjdhLTk4NDUtODE5ZWUwMWExNGRhLnBuZw=="}, "tarsSlug": "rt-nav-list-cf-picks"}, "certifiedFreshMovieInTheater":{ "header": null, "media":{ "url": "\u002Fm\u002Ftwinless", "name": "Twinless", "score": 98, "posterImg": "https:\u002F\u002Fresizing.flixster.com\u002Fj7lw2KeY9_XyfZQdqRZGku7_9C8=\u002F206x305\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FuxoeWz7uWmeYIV94_SzEV_osqe4=\u002Ffit-in\u002F180x240\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FVlylB3xT2RIYmRivMx37O3yD76Q=\u002Fems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2ZlNDQ1MGQ5LTFjN2QtNDIwNC04NWE1LTM5NGM4N2U5ZTgzYy5qcGc="}}, "certifiedFreshMovieInTheater4":{ "header": null, "media":{ "url": "\u002Fm\u002Fhamilton_2020", "name": "Hamilton", "score": 98, "posterImg": "https:\u002F\u002Fresizing.flixster.com\u002F1woquJmQfEhWCZtm7GcH0NMHsYA=\u002F206x305\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FPeAJ5ZpF5qB98ZiX6ixNDCgW2P0=\u002Ffit-in\u002F180x240\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FVmBvlTk8-z7pQvDZXTgSdj93WDE=\u002Fems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzkzY2IxZjFkLTE1NjEtNDQ4Yi05NDY3LTcxNzFmMDVhMDczNi5qcGc="}}, "certifiedFreshMovieAtHome":{ "header": null, "media":{ "url": "\u002Fm\u002Fthe_thursday_murder_club", "name": "The Thursday Murder Club", "score": 76, "posterImg": "https:\u002F\u002Fresizing.flixster.com\u002FjeeldFGcfSMgG09ey5VB7TCFiek=\u002F206x305\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002F9LXDkCzIBBNEiPURkB9t6VefF5Q=\u002Ffit-in\u002F180x240\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FrwdeR5xIiN0k7SWr6yXdnmb6zP8=\u002Fems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2EzYWFkZWJiLWE5N2MtNDc3MS1iMDRlLTk0YWVlYzI5M2UxZS5qcGc="}}, "tarsSlug": "rt-nav-list-cf-picks"}, "tvLists":{ "newTvTonight":{ "tarsSlug": "rt-hp-text-list-3", "title": "New TV Tonight", "shows": [{ "title": "Task: Season 1", "tomatometer":{ "tomatometer": 89, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Ftask\u002Fs01"},{ "title": "The Walking Dead: Daryl Dixon: Season 3", "tomatometer":{ "tomatometer": 80, "sentiment": "positive", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fthe_walking_dead_daryl_dixon\u002Fs03"},{ "title": "The Crow Girl: Season 1", "tomatometer":{ "tomatometer": 80, "sentiment": "positive", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fthe_crow_girl\u002Fs01"},{ "title": "Only Murders in the Building: Season 5", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fonly_murders_in_the_building\u002Fs05"},{ "title": "The Girlfriend: Season 1", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fthe_girlfriend\u002Fs01"},{ "title": "aka Charlie Sheen: Season 1", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Faka_charlie_sheen\u002Fs01"},{ "title": "Wizards Beyond Waverly Place: Season 2", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fwizards_beyond_waverly_place\u002Fs02"},{ "title": "Seen & Heard: the History of Black Television: Season 1", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fseen_and_heard_the_history_of_black_television\u002Fs01"},{ "title": "The Fragrant Flower Blooms With Dignity: Season 1", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fthe_fragrant_flower_blooms_with_dignity\u002Fs01"},{ "title": "Guts & Glory: Season 1", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fguts_and_glory\u002Fs01"}]}, "mostPopularTvOnRt":{ "tarsSlug": "rt-hp-text-list-2", "title": "Most Popular TV on RT", "shows": [{ "title": "The Paper: Season 1", "tomatometer":{ "tomatometer": 83, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fthe_paper_2025\u002Fs01"},{ "title": "Dexter: Resurrection: Season 1", "tomatometer":{ "tomatometer": 95, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fdexter_resurrection\u002Fs01"},{ "title": "Alien: Earth: Season 1", "tomatometer":{ "tomatometer": 95, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Falien_earth\u002Fs01"},{ "title": "Task: Season 1", "tomatometer":{ "tomatometer": 89, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Ftask\u002Fs01"},{ "title": "Wednesday: Season 2", "tomatometer":{ "tomatometer": 87, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fwednesday\u002Fs02"},{ "title": "Peacemaker: Season 2", "tomatometer":{ "tomatometer": 99, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fpeacemaker_2022\u002Fs02"},{ "title": "The Terminal List: Dark Wolf: Season 1", "tomatometer":{ "tomatometer": 73, "sentiment": "positive", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fthe_terminal_list_dark_wolf\u002Fs01"},{ "title": "Hostage: Season 1", "tomatometer":{ "tomatometer": 82, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fhostage_2025\u002Fs01"},{ "title": "Chief of War: Season 1", "tomatometer":{ "tomatometer": 93, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fchief_of_war\u002Fs01"},{ "title": "Irish Blood: Season 1", "tomatometer":{ "tomatometer": 100, "sentiment": "positive", "certified": false}, "tvPageUrl": "\u002Ftv\u002Firish_blood\u002Fs01"}]}}}, "links":{ "moviesInTheaters":{ "certifiedFresh": "\u002Fbrowse\u002Fmovies_in_theaters\u002Fcritics:certified_fresh~sort:popular", "comingSoon": "\u002Fbrowse\u002Fmovies_coming_soon\u002F", "openingThisWeek": "\u002Fbrowse\u002Fmovies_in_theaters\u002Fsort:newest", "title": "\u002Fbrowse\u002Fmovies_in_theaters", "topBoxOffice": "\u002Fbrowse\u002Fmovies_in_theaters"}, "onDvdAndStreaming":{ "all": "\u002Fbrowse\u002Fmovies_at_home\u002F", "certifiedFresh": "\u002Fbrowse\u002Fmovies_at_home\u002Fcritics:certified_fresh", "title": "\u002Fbrowse\u002Fmovies_at_home\u002F", "top": "\u002Fbrowse\u002Fmovies_at_home\u002Fsort:popular"}, "moreMovies":{ "topMovies": "\u002Fbrowse\u002Fmovies_at_home\u002Fsort:popular", "trailers": "\u002Ftrailers"}, "tvTonight": "\u002Fbrowse\u002Ftv_series_browse\u002Fsort:newest", "tvPopular": "\u002Fbrowse\u002Ftv_series_browse\u002Fsort:popular", "moreTv":{ "topTv": "\u002Fbrowse\u002Ftv_series_browse\u002Fsort:popular", "certifiedFresh": "\u002Fbrowse\u002Ftv_series_browse\u002Fcritics:fresh"}, "editorial":{ "allTimeLists": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fall-time-lists\u002F", "bingeGuide": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fbinge-guide\u002F", "comicsOnTv": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fcomics-on-tv\u002F", "countdown": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fcountdown\u002F", "fiveFavoriteFilms": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Ffive-favorite-films\u002F", "videoInterviews": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fvideo-interviews\u002F", "weekendBoxOffice": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fweekend-box-office\u002F", "weeklyKetchup": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fweekly-ketchup\u002F", "whatToWatch": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwhat-to-watch\u002F"}, "advertisingFooter": "https:\u002F\u002Ftogether.nbcuni.com\u002Fadvertise\u002F?utm_source=rotten_tomatoes&utm_medium=referral&utm_campaign=property_ad_pages&utm_content=footer", "californiaNotice": "https:\u002F\u002Fwww.nbcuniversalprivacy.com\u002Fprivacy\u002Fcalifornia-consumer-privacy-act", "careers": "\u002F\u002Fwww.fandango.com\u002Fcareers", "cookieManagement": "https:\u002F\u002Fwww.nbcuniversalprivacy.com\u002Fprivacy\u002Fcookies#accordionheader2", "fandangoAbout": "https:\u002F\u002Fwww.fandango.com\u002Fabout-us", "privacyPolicy": "https:\u002F\u002Fwww.nbcuniversalprivacy.com\u002Ffandango-privacy-policy", "termsPolicies": "\u002Fpolicies\u002Fterms-and-policies"}}; root.BK={ "PageName": "http:\u002F\u002Fwww.rottentomatoes.com\u002Fm\u002Ffriendly-fire2006", "SiteID": 37528, "SiteSection": ""}; root.RottenTomatoes.thirdParty={ "chartBeat":{ "auth": "64558", "domain": "rottentomatoes.com"}, "mpx":{ "accountPid": "NGweTC", "playerPid": "y__7B0iQTi4P", "playerPidPDK6": "pdk6_y__7B0iQTi4P", "accountId": "2474312077"}, "algoliaSearch":{ "aId": "79FRDP12PN", "sId": "175588f6e5f8319b27702e4cc4013561"}, "cognito":{ "upId": "us-west-2_4L0ZX4b1U", "clientId": "7pu48v8i2n25t4vhes0edck31c"}}; root.RottenTomatoes.serviceWorker={ "isServiceWokerOn": true}; root.__RT__ || (root.__RT__={}); root.__RT__.featureFlags={ "adsCarouselHP": false, "adsCarouselHPSlug": "rt-sponsored-carousel-list-mcdonalds-hp", "adsCarouselOP": false, "adsCarouselOPSlug": "rt-sponsored-carousel-list-mcdonalds-op", "adsMockDLP": false, "adsPages": "none", "adsSponsoredOverrideOP": true, "adsSponsoredOverrideOPSlugs": "rt-sponsored-override-op-starz", "adsVideoSpotlightHP": false, "appleSigninEnabled": true, "artiEnabled": true, "authPasswordEnabled": true, "authVerboseLogs": false, "bypassCriticValidationEnabled": false, "castAndCrewEnabled": true, "cookieConsentServiceEnabled": false, "crssoEnabled": false, "editorialApiDisabled": false, "faqUpdatesEnabled": true, "legacyBridge": true, "logVerboseEnabled": false, "mobileAppAndroid": "https:\u002F\u002Fplay.google.com\u002Fstore\u002Fapps\u002Fdetails?id=com.rottentomatoes.android", "mobileAppIos": "https:\u002F\u002Fapps.apple.com\u002Fus\u002Fapp\u002Frotten-tomatoes-movies-tv\u002Fid6673916573", "mobileAppIosMeta": "app-id=6673916573, app-argument=https:\u002F\u002Fwww.rottentomatoes.com\u002F", "mobileNavEnabled": true, "oneTrustJwtApiUrl": "https:\u002F\u002Fonetrustjwt.services.fandango.com", "oneTrustJwtServiceEnabled": false, "pageJsonEnabled": false, "profilesFeaturesEnabled": false, "profilesUsernameEnabled": false, "redesignMediaHeroEnabled": true, "redesignMoreLikeThis": true, "redesignSortTable": true, "trafficAndroidEnabled": false, "trafficSafariEnabled": true, "userMigrationEnabled": true, "versantFreewheelEnabled": false, "versantMpsDomain": "app.mps.vsnt.net", "versantMpsEnabled": false, "versantOneTrustScriptBlock": "\u003C!-- OneTrust Cookies Consent Notice start for rottentomatoes.com --\u003E \u003Cscript src=\"https:\u002F\u002Fcdn.cookielaw.org\u002Fconsent\u002F01978557-1604-76a7-ad7c-18216757cf52-test\u002FotSDKStub.js\" type=\"text\u002Fjavascript\" charset=\"UTF-8\" data-domain-script=\"01978557-1604-76a7-ad7c-18216757cf52-test\" integrity=\"sha384-Exfxdyaw5OnsUlHEKlNlz7OwgVCyLlitAtJsDmSNh3LeLlCjWXos3X\u002FCMNUbQ\u002FgA\" crossorigin=\"anonymous\" \u003E\u003C\u002Fscript\u003E \u003Cscript type=\"text\u002Fjavascript\"\u003E function OptanonWrapper(){ if (OnetrustActiveGroups.includes('7')){ document.querySelector('search-results-nav-manager')?.setAlgoliaInsightUserToken?.();}} \u003C\u002Fscript\u003E \u003C!-- OneTrust Cookies Consent Notice end for rottentomatoes.com --\u003E \u003C!-- OneTrust IAB US Privacy (USP) --\u003E \u003Cscript src=\"https:\u002F\u002Fcdn.cookielaw.org\u002Fopt-out\u002FotCCPAiab.js\" id=\"privacyCookie\" type=\"text\u002Fjavascript\" charset=\"UTF-8\" ccpa-opt-out-ids=\"USP\" ccpa-opt-out-geo=\"US\" ccpa-opt-out-lspa=\"false\"\u003E\u003C\u002Fscript\u003E \u003C!-- OneTrust IAB US Privacy (USP) end --\u003E", "videoGeoFencingEnabled": true}; root.RottenTomatoes.dtmData={ "webVersion": "node", "rtVersion": 3.1, "loggedInStatus": "", "customerId": "", "pageName": "rt | movies | overview", "titleType": "Movie"}; root.RottenTomatoes.context.adsMockDLP=false; root.RottenTomatoes.context.gptSite="search";}(this)); </script><script fetchpriority="high" src="/assets/pizza-pie/javascripts/bundles/roma/preload.18bcfff8e54.js"></script><script src="/assets/pizza-pie/javascripts/bundles/roma/vendors.a4cc402b78a.js"></script><script src="/assets/pizza-pie/javascripts/bundles/roma/default.24dc1977289.js"></script><script async data-SearchResultsNavManager="script:load" src="https://cdn.jsdelivr.net/npm/algoliasearch@4/dist/algoliasearch-lite.umd.js"></script><script src="/assets/pizza-pie/javascripts/templates/roma/searchNav.a3288ea5efe.js"></script><script src="/assets/pizza-pie/javascripts/bundles/roma/searchNav.6a836b4ca81.js"></script><script src="/assets/pizza-pie/javascripts/bundles/search.3cbb7fc9cd1.js"></script><script>if (window.mps && typeof window.mps.writeFooter==='function'){ window.mps.writeFooter();} </script><script>window._satellite && _satellite.pageBottom(); </script></body></html> 1 + <!DOCTYPE html><html lang="en" dir="ltr" xmlns="http://www.w3.org/1999/xhtml" prefix="fb: http://www.facebook.com/2008/fbml og: http://opengraphprotocol.org/schema/"><head prefix="og: http://ogp.me/ns# flixstertomatoes: http://ogp.me/ns/apps/flixstertomatoes#"><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><meta http-equiv="x-ua-compatible" content="ie=edge"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="shortcut icon" sizes="76x76" type="image/x-icon" href="https://www.rottentomatoes.com/assets/pizza-pie/images/favicon.ico" /><title>Search Results | Rotten Tomatoes</title><meta name="description" content="Rotten Tomatoes, home of the Tomatometer, is the most trusted measurement of quality for Movies & TV. The definitive site for Reviews, Trailers, Showtimes, and Tickets"><meta property="fb:app_id" content=""><meta property="og:site_name" content="Rotten Tomatoes"><meta property="og:title" content="Search Results"><meta property="og:description" content="Rotten Tomatoes, home of the Tomatometer, is the most trusted measurement of quality for Movies & TV. The definitive site for Reviews, Trailers, Showtimes, and Tickets"><meta property="og:type" content=""><meta property="og:url" content=""><meta property="og:image" content="https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/RT_TwitterCard_2018.jpg"><meta property="og:locale" content="en_US"><meta name="twitter:card" content="summary_large_image"><meta name="twitter:image" content="https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/RT_TwitterCard_2018.jpg"><meta name="twitter:title" content="Search Results"><meta name="twitter:text:title" content="Search Results"><meta name="twitter:description" content="Rotten Tomatoes, home of the Tomatometer, is the most trusted measurement of quality for Movies & TV. The definitive site for Reviews, Trailers, Showtimes, and Tickets"><meta name="twitter:site" content="@rottentomatoes"></head><body class="body no-touch js-mptd-layout" data-AdsGlobalSkinTakeoverManager="body" data-SearchResultsNavManager="body"><div id="main" class="container rt-layout__body"><main id="main_container" class="container rt-layout__content"><div id="main-page-content"><div class="search__container layout"><section class="search__main layout__column layout__column--main"><h1 class="unset"><rt-text context="heading" size="1.625">Search Results for : "Fantastic Four"</rt-text></h1><search-page-manager searchQuery="Fantastic Four"></search-page-manager><div id="search-results" data-qa="search-results"><search-page-result skeleton="panel" type="movie" data-qa="search-result"><h2 class="unset" slot="title" data-qa="search-result-title"><rt-text context="heading" size="1.25">Movies </rt-text></h2><button slot="prev-btn" data-qa="paging-btn-prev">Prev</button><button slot="next-btn" data-qa="paging-btn-next">Next</button><ul slot="list"><search-page-media-row skeleton="panel" cast="Pedro Pascal,Vanessa Kirby,Ebon Moss-Bachrach" data-qa="data-row" endyear="" releaseyear="2025" startyear="" tomatometeriscertified="true" tomatometerscore="87" tomatometersentiment="POSITIVE"><a href="https://www.rottentomatoes.com/m/the_fantastic_four_first_steps" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="The Fantastic Four: First Steps" loading="lazy" src="https://resizing.flixster.com/kh9TONIlIUrcSELHRnWw0K1u3Cg=/fit-in/80x126/v2/https://resizing.flixster.com/WEfe-vTCqjHioD77J7f-qeoZZdY=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2FjN2M2NzA0LWNiMTctNDhjMy05N2VlLTk3YWI3N2JhZDZmMS5qcGc="></a><a href="https://www.rottentomatoes.com/m/the_fantastic_four_first_steps" class="unset" data-qa="info-name" slot="title">The Fantastic Four: First Steps </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Miles Teller,Michael B. Jordan,Kate Mara" data-qa="data-row" endyear="" releaseyear="2015" startyear="" tomatometeriscertified="false" tomatometerscore="9" tomatometersentiment="NEGATIVE"><a href="https://www.rottentomatoes.com/m/fantastic_four_2015" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Fantastic Four" loading="lazy" src="https://resizing.flixster.com/Dh-AxpIB7cnywbmjUyGBmgB1hFU=/fit-in/80x126/v2/https://resizing.flixster.com/EoFPwXUATnha798E0UJc9MpHV5I=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2U1ZjRiZDE1LWFmNjUtNGM3ZS1hMDIwLWQ3YjYzYTU0N2Y5NC5qcGc="></a><a href="https://www.rottentomatoes.com/m/fantastic_four_2015" class="unset" data-qa="info-name" slot="title">Fantastic Four </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Ioan Gruffudd,Jessica Alba,Chris Evans" data-qa="data-row" endyear="" releaseyear="2005" startyear="" tomatometeriscertified="false" tomatometerscore="28" tomatometersentiment="NEGATIVE"><a href="https://www.rottentomatoes.com/m/fantastic_four" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Fantastic Four" loading="lazy" src="https://resizing.flixster.com/3__TW0ho8WMbL2BtJlUY4__V7cM=/fit-in/80x126/v2/https://resizing.flixster.com/bWXub3N0V15ZoiGrLkZ5qSPmhVc=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2FlNTAzYmE0LTI2NjctNGQ2OS05MDNhLTAxYzVhM2I3NWU1OC5qcGc="></a><a href="https://www.rottentomatoes.com/m/fantastic_four" class="unset" data-qa="info-name" slot="title">Fantastic Four </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Ioan Gruffudd,Jessica Alba,Chris Evans" data-qa="data-row" endyear="" releaseyear="2007" startyear="" tomatometeriscertified="false" tomatometerscore="37" tomatometersentiment="NEGATIVE"><a href="https://www.rottentomatoes.com/m/fantastic_four_rise_of_the_silver_surfer" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Fantastic Four: Rise of the Silver Surfer" loading="lazy" src="https://resizing.flixster.com/MHurLD6EaKJSCaBEVJAQEBl51j4=/fit-in/80x126/v2/https://resizing.flixster.com/GX2x8tQN7yQMlIUbQ1k4-EnpTAw=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzc0ZTIzZWQ2LTdmNmYtNGZjNS04NmMyLTA2ZTJjMzFjMjM5Yi5qcGc="></a><a href="https://www.rottentomatoes.com/m/fantastic_four_rise_of_the_silver_surfer" class="unset" data-qa="info-name" slot="title">Fantastic Four: Rise of the Silver Surfer </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Alex Hyde-White,Jay Underwood,Rebecca Staab" data-qa="data-row" endyear="" releaseyear="1994" startyear="" tomatometeriscertified="false" tomatometerscore="33" tomatometersentiment="NEGATIVE"><a href="https://www.rottentomatoes.com/m/10005582-fantastic_four" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="The Fantastic Four" loading="lazy" src="https://resizing.flixster.com/k3h3tXzUOacsIjzSU46kNsG9jao=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p11974406_p_v10_aa.jpg"></a><a href="https://www.rottentomatoes.com/m/10005582-fantastic_four" class="unset" data-qa="info-name" slot="title">The Fantastic Four </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Carl Ciarfalio,Roger Corman,Joseph Culp" data-qa="data-row" endyear="" releaseyear="2015" startyear="" tomatometeriscertified="false" tomatometerscore="89" tomatometersentiment="POSITIVE"><a href="https://www.rottentomatoes.com/m/doomed_the_untold_story_of_roger_cormans_the_fantastic_four" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Doomed: The Untold Story of Roger Corman&#39;s the Fantastic Four" loading="lazy" src="https://resizing.flixster.com/oWBX3Tbq6dCxeofYxpduj-3eu1A=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p12021288_p_v8_ab.jpg"></a><a href="https://www.rottentomatoes.com/m/doomed_the_untold_story_of_roger_cormans_the_fantastic_four" class="unset" data-qa="info-name" slot="title">Doomed: The Untold Story of Roger Corman&#39;s the Fantastic Four </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Eddie Redmayne,Katherine Waterston,Dan Fogler" data-qa="data-row" endyear="" releaseyear="2018" startyear="" tomatometeriscertified="false" tomatometerscore="36" tomatometersentiment="NEGATIVE"><a href="https://www.rottentomatoes.com/m/fantastic_beasts_the_crimes_of_grindelwald" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Fantastic Beasts: The Crimes of Grindelwald" loading="lazy" src="https://resizing.flixster.com/jupUs-IsF4TwzhGk9zIXV5swiRQ=/fit-in/80x126/v2/https://resizing.flixster.com/47erqYRky74FKjCcwEjBZJsQO2s=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzc4M2IyOTU3LTYwMjEtNGY1Ni1hMTE4LTU3NGI5ZjVkYTM2My53ZWJw"></a><a href="https://www.rottentomatoes.com/m/fantastic_beasts_the_crimes_of_grindelwald" class="unset" data-qa="info-name" slot="title">Fantastic Beasts: The Crimes of Grindelwald </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Simon Pegg,Amara Karan,Clare Higgins" data-qa="data-row" endyear="" releaseyear="2012" startyear="" tomatometeriscertified="false" tomatometerscore="34" tomatometersentiment="NEGATIVE"><a href="https://www.rottentomatoes.com/m/a_fantastic_fear_of_everything" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="A Fantastic Fear of Everything" loading="lazy" src="https://resizing.flixster.com/GfeAPSdwANE5BnTTvY-9d_euTr8=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p9288765_p_v13_ae.jpg"></a><a href="https://www.rottentomatoes.com/m/a_fantastic_fear_of_everything" class="unset" data-qa="info-name" slot="title">A Fantastic Fear of Everything </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Mel Blanc,June Foray,Les Tremayne" data-qa="data-row" endyear="" releaseyear="1983" startyear="" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/m/daffy_ducks_movie_fantastic_island" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Daffy Duck&#39;s Movie: Fantastic Island" loading="lazy" src="https://resizing.flixster.com/fWIrhyzpAPSTAaSaVmyPBfQ5I5Y=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p6855_p_v13_ab.jpg"></a><a href="https://www.rottentomatoes.com/m/daffy_ducks_movie_fantastic_island" class="unset" data-qa="info-name" slot="title">Daffy Duck&#39;s Movie: Fantastic Island </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Christopher Lloyd,Sarah Michelle Gellar,Christopher Collet" data-qa="data-row" endyear="" releaseyear="2013" startyear="" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/m/freedom_force_2012" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Freedom Force" loading="lazy" src="https://resizing.flixster.com/YsSgG6rXnCT6eTYRepAogpgoQWI=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p10403283_p_v8_ae.jpg"></a><a href="https://www.rottentomatoes.com/m/freedom_force_2012" class="unset" data-qa="info-name" slot="title">Freedom Force </a></search-page-media-row></ul><button slot="more-btn" data-qa="search-more-btn">More Movies...</button></search-page-result><search-page-result skeleton="panel" type="tvSeries" data-qa="search-result"><h2 class="unset" slot="title" data-qa="search-result-title"><rt-text context="heading" size="1.25">TV shows </rt-text></h2><button slot="prev-btn" data-qa="paging-btn-prev">Prev</button><button slot="next-btn" data-qa="paging-btn-next">Next</button><ul slot="list"><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="2010" releaseyear="" startyear="2006" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/tv/fantastic_four_worlds_greatest_heroes" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Fantastic Four: World&#39;s Greatest Heroes" loading="lazy" src="https://resizing.flixster.com/G3rsmvYdoAMpKp7W0XhvnYtTnFA=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p384193_b_v8_af.jpg"></a><a href="https://www.rottentomatoes.com/tv/fantastic_four_worlds_greatest_heroes" class="unset" data-qa="info-name" slot="title">Fantastic Four: World&#39;s Greatest Heroes </a></search-page-media-row><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="" releaseyear="" startyear="" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/tv/fantastic_four_1994" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Fantastic Four" loading="lazy" src="https://resizing.flixster.com/-W5dxKVVhkgAIwQU6JHNp_TYNLU=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p384190_b_v8_aq.jpg"></a><a href="https://www.rottentomatoes.com/tv/fantastic_four_1994" class="unset" data-qa="info-name" slot="title">Fantastic Four </a></search-page-media-row><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="1968" releaseyear="" startyear="1967" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/tv/fantastic_four" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Fantastic Four" loading="lazy" src="https://resizing.flixster.com/0CqX0L9stEQ5Fwg1wrSCvV1L40Y=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p384191_b_v10_aa.jpg"></a><a href="https://www.rottentomatoes.com/tv/fantastic_four" class="unset" data-qa="info-name" slot="title">Fantastic Four </a></search-page-media-row><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="1978" releaseyear="" startyear="1978" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/tv/the_new_fantastic_four" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Fantastic Four" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="https://www.rottentomatoes.com/tv/the_new_fantastic_four" class="unset" data-qa="info-name" slot="title">Fantastic Four </a></search-page-media-row><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="1977" releaseyear="" startyear="1977" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/tv/the_fantastic_journey" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="The Fantastic Journey" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="https://www.rottentomatoes.com/tv/the_fantastic_journey" class="unset" data-qa="info-name" slot="title">The Fantastic Journey </a></search-page-media-row><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="2010" releaseyear="" startyear="2010" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/tv/fantasia_for_real" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Fantasia for Real" loading="lazy" src="https://resizing.flixster.com/xFR7z8b8SwD09sIUUHP9Vb6twQA=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p7967043_b_v8_aa.jpg"></a><a href="https://www.rottentomatoes.com/tv/fantasia_for_real" class="unset" data-qa="info-name" slot="title">Fantasia for Real </a></search-page-media-row></ul><button slot="more-btn" data-qa="search-more-btn">More TV Shows...</button></search-page-result><ad-unit hidden unit-display="mobile" unit-type="interscroller" no-retry><aside slot="ad-inject" class="center mobile-interscroller"></aside></ad-unit><search-page-result skeleton="panel" type="celebrity" data-qa="search-result"><h2 class="unset" slot="title" data-qa="search-result-title"><rt-text context="heading" size="1.25">Celebrities </rt-text></h2><button slot="prev-btn" data-qa="paging-btn-prev">Prev</button><button slot="next-btn" data-qa="paging-btn-next">Next</button><ul slot="list"><search-page-item-row skeleton="panel" data-qa="data-row"><a href="/celebrity/the_fantastic_four" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="The Fantastic Four" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="/celebrity/the_fantastic_four" class="unset" data-qa="info-name" slot="title"><span>The Fantastic Four</span></a></search-page-item-row><search-page-item-row skeleton="panel" data-qa="data-row"><a href="/celebrity/fantastic_our" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Fantastic รดur" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="/celebrity/fantastic_our" class="unset" data-qa="info-name" slot="title"><span>Fantastic รดur</span></a></search-page-item-row></ul><button slot="more-btn" data-qa="search-more-btn">More Celebrities...</button></search-page-result></div></section></div></div></main><footer class="footer container" data-PagePicturesManager="footer"></footer></div></body></html>
+1 -1
internal/services/samples/search.html
··· 1 - <!DOCTYPE html><html lang="en" dir="ltr" xmlns="http://www.w3.org/1999/xhtml" prefix="fb: http://www.facebook.com/2008/fbml og: http://opengraphprotocol.org/schema/"><head prefix="og: http://ogp.me/ns# flixstertomatoes: http://ogp.me/ns/apps/flixstertomatoes#"><script charset="UTF-8" crossorigin="anonymous" data-domain-script="7e979733-6841-4fce-9182-515fac69187f" integrity="sha384-TKdmlzVmoD70HzftTw4WtOzIBL5mNx8mXSRzEvwrWjpIJ7FZ/EuX758yMDWXtRUN" src="https://cdn.cookielaw.org/consent/7e979733-6841-4fce-9182-515fac69187f/otSDKStub.js" type="text/javascript"></script><script type="text/javascript">function OptanonWrapper(){ if (OnetrustActiveGroups.includes('7')){ document.querySelector('search-results-nav-manager')?.setAlgoliaInsightUserToken?.();}} </script><script ccpa-opt-out-ids="USP" ccpa-opt-out-geo="US" ccpa-opt-out-lspa="false" charset="UTF-8" src="https://cdn.cookielaw.org/opt-out/otCCPAiab.js" type="text/javascript"></script><script src="/assets/pizza-pie/javascripts/bundles/roma/rt-common.js?single"></script><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><meta http-equiv="x-ua-compatible" content="ie=edge"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="shortcut icon" sizes="76x76" type="image/x-icon" href="https://www.rottentomatoes.com/assets/pizza-pie/images/favicon.ico" /><title>Search Results | Rotten Tomatoes</title><meta name="description" content="Rotten Tomatoes, home of the Tomatometer, is the most trusted measurement of quality for Movies & TV. The definitive site for Reviews, Trailers, Showtimes, and Tickets"><meta property="fb:app_id" content=""><meta property="og:site_name" content="Rotten Tomatoes"><meta property="og:title" content="Search Results"><meta property="og:description" content="Rotten Tomatoes, home of the Tomatometer, is the most trusted measurement of quality for Movies & TV. The definitive site for Reviews, Trailers, Showtimes, and Tickets"><meta property="og:type" content=""><meta property="og:url" content=""><meta property="og:image" content="https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/RT_TwitterCard_2018.jpg"><meta property="og:locale" content="en_US"><meta name="twitter:card" content="summary_large_image"><meta name="twitter:image" content="https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/RT_TwitterCard_2018.jpg"><meta name="twitter:title" content="Search Results"><meta name="twitter:text:title" content="Search Results"><meta name="twitter:description" content="Rotten Tomatoes, home of the Tomatometer, is the most trusted measurement of quality for Movies & TV. The definitive site for Reviews, Trailers, Showtimes, and Tickets"><meta name="twitter:site" content="@rottentomatoes"><script>var dataLayer=dataLayer || []; dataLayer.push({ "webVersion": "node", "rtVersion": 3.1, "loggedInStatus": "", "customerId": ""}); </script><script id="mps-page-integration">window.mpscall={ "cag[score]": null, "cag[certified_fresh]": null, "cag[fresh_rotten]": null, "cag[rating]": null, "cag[release]": null, "cag[movieshow]": null, "cag[genre]": null, "cag[urlid]": null, "cat": "search|results", "field[env]": "production", "field[rtid]": null, "path": "/search", "site": "rottentomatoes-web", "title": "Search Results", "type": "results"}; var mpsopts={ 'host': 'mps.nbcuni.com', 'updatecorrelator': 1}; var mps=mps ||{}; mps._ext=mps._ext ||{}; mps._adsheld=[]; mps._queue=mps._queue ||{}; mps._queue.mpsloaded=mps._queue.mpsloaded || []; mps._queue.mpsinit=mps._queue.mpsinit || []; mps._queue.gptloaded=mps._queue.gptloaded || []; mps._queue.adload=mps._queue.adload || []; mps._queue.adclone=mps._queue.adclone || []; mps._queue.adview=mps._queue.adview || []; mps._queue.refreshads=mps._queue.refreshads || []; mps.__timer=Date.now || function (){ return +new Date}; mps.__intcode="v2"; if (typeof mps.getAd !="function") mps.getAd=function (adunit){ if (typeof adunit !="string") return false; var slotid="mps-getad-" + adunit.replace(/\W/g, ""); if (!mps._ext || !mps._ext.loaded){ mps._queue.gptloaded.push(function (){ typeof mps._gptfirst=="function" && mps._gptfirst(adunit, slotid); mps.insertAd("#" + slotid, adunit)}); mps._adsheld.push(adunit)} return '<div id="' + slotid + '" class="mps-wrapper" data-mps-fill-slot="' + adunit + '"></div>'}; </script><script src="//mps.nbcuni.com/fetch/ext/load-rottentomatoes-web.js?nowrite=2" id="mps-load"></script><link rel="manifest" href="https://www.rottentomatoes.com/assets/pizza-pie/manifest/manifest.json" /><link rel="apple-touch-icon" href="https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/apple-touch-icon-60.jpg" /><link rel="apple-touch-icon" sizes="152x152" href="https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/apple-touch-icon-152.jpg" /><link rel="apple-touch-icon" sizes="167x167" href="https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/apple-touch-icon-167.jpg" /><link rel="apple-touch-icon" sizes="180x180" href="https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/apple-touch-icon-180.jpg" /><meta name="apple-itunes-app" content="app-id=6673916573, app-argument=https://www.rottentomatoes.com/"><meta name="google-site-verification" content="VPPXtECgUUeuATBacnqnCm4ydGO99reF-xgNklSbNbc" /><meta name="msvalidate.01" content="034F16304017CA7DCF45D43850915323" /><meta name="theme-color" content="#FA320A"><meta http-equiv="x-dns-prefetch-control" content="on"><link rel="dns-prefetch" href="//www.rottentomatoes.com" /><link rel="preconnect" href="//www.rottentomatoes.com" /><link rel="stylesheet" href="/assets/pizza-pie/stylesheets/bundles/layouts/default.b6077682d41.css" /><link rel="preload" href="/assets/pizza-pie/stylesheets/bundles/pages/search.2697d48faa3.css" as="style" onload="this.onload=null;this.rel='stylesheet'" /><script>window.RottenTomatoes={}; window.RTLocals={}; window.nunjucksPrecompiled={}; window.__RT__={}; </script><script src="https://cdn.jwplayer.com/libraries/U8MHzHHR.js"></script><script src="https://sb.scorecardresearch.com/c2/plugins/streamingtag_plugin_jwplayer.js"></script><script>!function (e){ var n="https://s.go-mpulse.net/boomerang/"; if ("False"=="True") e.BOOMR_config=e.BOOMR_config ||{}, e.BOOMR_config.PageParams=e.BOOMR_config.PageParams ||{}, e.BOOMR_config.PageParams.pci=!0, n="https://s2.go-mpulse.net/boomerang/"; if (window.BOOMR_API_key="4RDDZ-2Z6GP-RRNMC-PYEUL-SK6K9", function (){ function e(){ if (!o){ var e=document.createElement("script"); e.id="boomr-scr-as", e.src=window.BOOMR.url, e.async=!0, i.parentNode.appendChild(e), o=!0}} function t(e){ o=!0; var n, t, a, r, d=document, O=window; if (window.BOOMR.snippetMethod=e ? "if" : "i", t=function (e, n){ var t=d.createElement("script"); t.id=n || "boomr-if-as", t.src=window.BOOMR.url, BOOMR_lstart=(new Date).getTime(), e=e || d.body, e.appendChild(t)}, !window.addEventListener && window.attachEvent && navigator.userAgent.match(/MSIE [67]\./)) return window.BOOMR.snippetMethod="s", void t(i.parentNode, "boomr-async"); a=document.createElement("IFRAME"), a.src="about:blank", a.title="", a.role="presentation", a.loading="eager", r=(a.frameElement || a).style, r.width=0, r.height=0, r.border=0, r.display="none", i.parentNode.appendChild(a); try{ O=a.contentWindow, d=O.document.open()} catch (_){ n=document.domain, a.src="javascript:var d=document.open();d.domain='" + n + "';void(0);", O=a.contentWindow, d=O.document.open()} if (n) d._boomrl=function (){ this.domain=n, t()}, d.write("<bo" + "dy onload='document._boomrl();'>"); else if (O._boomrl=function (){ t()}, O.addEventListener) O.addEventListener("load", O._boomrl, !1); else if (O.attachEvent) O.attachEvent("onload", O._boomrl); d.close()} function a(e){ window.BOOMR_onload=e && e.timeStamp || (new Date).getTime()} if (!window.BOOMR || !window.BOOMR.version && !window.BOOMR.snippetExecuted){ window.BOOMR=window.BOOMR ||{}, window.BOOMR.snippetStart=(new Date).getTime(), window.BOOMR.snippetExecuted=!0, window.BOOMR.snippetVersion=12, window.BOOMR.url=n + "4RDDZ-2Z6GP-RRNMC-PYEUL-SK6K9"; var i=document.currentScript || document.getElementsByTagName("script")[0], o=!1, r=document.createElement("link"); if (r.relList && "function"==typeof r.relList.supports && r.relList.supports("preload") && "as" in r) window.BOOMR.snippetMethod="p", r.href=window.BOOMR.url, r.rel="preload", r.as="script", r.addEventListener("load", e), r.addEventListener("error", function (){ t(!0)}), setTimeout(function (){ if (!o) t(!0)}, 3e3), BOOMR_lstart=(new Date).getTime(), i.parentNode.appendChild(r); else t(!1); if (window.addEventListener) window.addEventListener("load", a, !1); else if (window.attachEvent) window.attachEvent("onload", a)}}(), "".length >0) if (e && "performance" in e && e.performance && "function"==typeof e.performance.setResourceTimingBufferSize) e.performance.setResourceTimingBufferSize(); !function (){ if (BOOMR=e.BOOMR ||{}, BOOMR.plugins=BOOMR.plugins ||{}, !BOOMR.plugins.AK){ var n=""=="true" ? 1 : 0, t="", a="eyd6zaauaeceajqacqcoyaaaevul3lwp-f-65027684e-clienttons-s.akamaihd.net", i="false"=="true" ? 2 : 1, o={ "ak.v": "39", "ak.cp": "1839344", "ak.ai": parseInt("1226367", 10), "ak.ol": "0", "ak.cr": 17, "ak.ipv": 6, "ak.proto": "h2", "ak.rid": "3b031e30", "ak.r": 43885, "ak.a2": n, "ak.m": "dsca", "ak.n": "essl", "ak.bpcip": "2607:ec80:1401:440::", "ak.cport": 62968, "ak.gh": "23.199.45.9", "ak.quicv": "", "ak.tlsv": "tls1.3", "ak.0rtt": "", "ak.0rtt.ed": "", "ak.csrc": "-", "ak.acc": "", "ak.t": "1757261519", "ak.ak": "hOBiQwZUYzCg5VSAfCLimQ==Rujch2H8VUxen6Rm4ZRpLwpby86LIteu33YpY/FAcDRzHLn1OxkDzEfdH28VpGCMeDIq68sAsxquz9zSL8+LD+C/AYT1n4ikVTU0o6vxrSz8PsmzR/7UqRqnWmtkbjIgDLxgKficxihMzEnMDg/iCNKIev6M39p7hxr6UZw4jal++zii8ccIjmbdqGveRzZspVB29H8R3ssqTXUifnE9RcTQTTXAliNSlVhDlreixNX8AbB6fQuUmOdFwmI6ZpgekLrWSVcAr823PVRO5M+HoMq3ZnWlh+6map1NSRqiPpcY4Ne9g7/bHrVbwA90y2fShaz8tJn8/K5CgtkjR3gaXJ0MUhSKRHywWDnCnd/vRXbbfP9qUWKujdQB1jHIe+ke4TdTX1DXNWLqmNCjmi53JPztgmnZcS9px1qB1xdu7kI=", "ak.pv": "3", "ak.dpoabenc": "", "ak.tf": i}; if ("" !==t) o["ak.ruds"]=t; var r={ i: !1, av: function (n){ var t="http.initiator"; if (n && (!n[t] || "spa_hard"===n[t])) o["ak.feo"]=void 0 !==e.aFeoApplied ? 1 : 0, BOOMR.addVar(o)}, rv: function (){ var e=["ak.bpcip", "ak.cport", "ak.cr", "ak.csrc", "ak.gh", "ak.ipv", "ak.m", "ak.n", "ak.ol", "ak.proto", "ak.quicv", "ak.tlsv", "ak.0rtt", "ak.0rtt.ed", "ak.r", "ak.acc", "ak.t", "ak.tf"]; BOOMR.removeVar(e)}}; BOOMR.plugins.AK={ akVars: o, akDNSPreFetchDomain: a, init: function (){ if (!r.i){ var e=BOOMR.subscribe; e("before_beacon", r.av, null, null), e("onbeacon", r.rv, null, null), r.i=!0} return this}, is_complete: function (){ return !0}}}}()}(window);</script></head><body class="body no-touch js-mptd-layout" data-AdsGlobalSkinTakeoverManager="body" data-SearchResultsNavManager="body"><cookie-manager></cookie-manager><device-inspection-manager endpoint="https://www.rottentomatoes.com/napi/device/inspection"></device-inspection-manager><user-activity-manager profiles-features-enabled="false"></user-activity-manager><user-identity-manager profiles-features-enabled="false"></user-identity-manager><ad-unit-manager></ad-unit-manager><auth-initiate-manager profiles-username-enabled="false" data-ArtiManager="authInitiateManager" data-WatchlistButtonManager="authInitiateManager:createAccount"></auth-initiate-manager><auth-profile-manager data-AuthInitiateManager="authProfileManager"></auth-profile-manager><auth-validation-manager data-AuthInitiateManager="authValidation"></auth-validation-manager><overlay-base data-AuthInitiateManager="overlayBase:close" data-PagePollsIndexManager="authOverlay:close" hidden><overlay-flows data-AuthInitiateManager="overlayFlows" slot="content"><action-icon slot="close" class="auth-overlay__icon-button auth-overlay__icon-button--close" aria-label="Close" data-qa="close-overlay-btn" icon="close"></action-icon></overlay-flows></overlay-base><notification-alert data-AuthInitiateManager="authSuccess" animate hidden><rt-icon icon="check-circled"></rt-icon><span>Signed in</span></notification-alert><div id="auth-templates" data-AuthInitiateManager="authTemplates"><template slot="screens" id="account-create-username-screen"><account-create-username-screen data-qa="account-create-username-screen"><input-label slot="input-username" state="default" data-qa="username-input-label"><label slot="label" for="create-username-input">Username</label><input slot="input" id="create-username-input" type="text" placeholder="Username" data-qa="username-input" /></input-label><rt-button disabled slot="btn-continue" shape="pill" data-qa="continue-btn">Continue</rt-button><rt-text class="terms-and-policies" slot="terms-and-policies" size="0.75">By joining, you agree to the <rt-link href="/policies/terms-and-policies" target="_blank" data-qa="terms-policies-link">Terms and Policies</rt-link>and the <rt-link href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" data-qa="privacy-policy-link">Privacy Policy</rt-link>and to receive email from <rt-link href="https://www.fandango.com/about-us" target="_blank" data-qa="about-fandango-link">Fandango Media Brands</rt-link>. </rt-text></account-create-username-screen></template><template slot="screens" id="account-email-change-screen"><account-email-change-screen data-qa="account-email-change-screen" email="user@email.com"><input-label class="new-email-input" state="default" slot="new-email-input" data-qa="email-input-label"><label slot="label" for="newEmail">Enter new email</label><input slot="input" name="newEmail" type="text" placeholder="Enter new email" autocomplete="off" data-qa="email-input"></input></input-label><rt-button slot="submit-button" disabled shape="pill" data-qa="submit-btn">Submit</rt-button><rt-text class="terms-and-policies" slot="terms-and-policies" size="0.75">By joining, you agree to the <rt-link href="/policies/terms-and-policies" target="_blank" data-qa="terms-policies-link">Terms and Policies</rt-link>and <rt-link href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" data-qa="privacy-policy-link">Privacy Policy</rt-link>and to receive email from the <rt-link href="https://www.fandango.com/about-us" target="_blank" data-qa="about-fandango-link">Fandango Media Brands</rt-link>. </rt-text></account-email-change-screen></template><template slot="screens" id="account-email-change-success-screen"><account-email-change-success-screen data-qa="login-create-success-screen"><rt-text slot="message" size="1.5" style="--fontWeight: var(--franklinGothicDemi);">Email change successful</rt-text><rt-text slot="submessage">You are signed out for your security. </br>Please sign in again.</rt-text><img slot="icon" src="/assets/pizza-pie/images/icons/cognito-success.3a3d5f32fab.svg" width="111" height="105" /></account-email-change-success-screen></template><template slot="screens" id="account-password-change-screen"><account-password-change-screen data-qa="account-password-change-screen"><input-label state="default" slot="input-password-existing"><label slot="label" for="password-existing">Existing password</label><input slot="input" name="password-existing" type="password" placeholder="Enter existing password" autocomplete="off"></input></input-label><input-label state="default" slot="input-password-new"><label slot="label" for="password-new">New password</label><input slot="input" name="password-new" type="password" placeholder="Enter new password" autocomplete="off"></input></input-label><rt-button disabled shape="pill" slot="submit-button">Submit</rt-button></account-password-change-screen></template><template slot="screens" id="account-password-change-updating-screen"><login-success-screen data-qa="account-password-change-updating-screen" hidebanner><rt-text slot="status" size="1.5" style="--fontWeight: var(--franklinGothicDemi);">Updating your password... </rt-text><img slot="icon" src="/assets/pizza-pie/images/icons/cognito-success.3a3d5f32fab.svg" width="111" height="105" /></login-success-screen></template><template slot="screens" id="account-password-change-success-screen"><login-success-screen data-qa="account-password-change-success-screen" hidebanner><rt-text slot="status" size="1.5" style="--fontWeight: var(--franklinGothicDemi);">Success! </rt-text><img slot="icon" src="/assets/pizza-pie/images/icons/cognito-success.3a3d5f32fab.svg" width="111" height="105" /></login-success-screen></template><template slot="screens" id="account-verifying-email-screen"><account-verifying-email-screen data-qa="account-verifying-email-screen"><img slot="icon" src="/assets/pizza-pie/images/icons/cognito-auth-verify.e74a69e9a77.svg" alt="email" /><rt-text slot="status">Verifying your email... </rt-text><rt-button type="cta-large" href="/?authFlowScreen=loginStartScreen" slot="retryLink" size="0.875" style="--fontWeight: var(--franklinGothicMedium);" data-qa="retry-link">Retry </rt-button></account-verifying-email-screen></template><template slot="screens" id="cognito-loading"><div><loading-spinner id="cognito-auth-loading-spinner"></loading-spinner><style>#cognito-auth-loading-spinner{ font-size: 2rem; transform: translate(calc(100% - 1em), 250px); width: 50%;} </style></div></template><template slot="screens" id="login-check-email-screen"><login-check-email-screen data-qa="login-check-email-screen" email="user@email.com"><rt-text class="note-text" size="1" slot="noteText">Please open the email link from the same browser you initiated the change email process from. </rt-text><rt-text slot="gotEmailMessage" size="0.875">Didn't you get the email? </rt-text><rt-button slot="resendEmailLink" size="0.875" type="cta-large" data-qa="resend-email-link">Resend email </rt-button><rt-link context="label" slot="troubleLoginLink" size="0.875" href="/reset-client" data-qa="reset-link">Having trouble logging in?</rt-link><rt-text class="terms-and-policies" slot="termsAndPolicies" size="0.75">By continuing, you agree to the <rt-link href="/policies/terms-and-policies" target="_blank" data-qa="terms-policies-link" style="--textColor: var(--blueLink);">Terms and Policies</rt-link>and the <rt-link href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" data-qa="privacy-policy-link" style="--textColor: var(--blueLink);">Privacy Policy</rt-link>and to receive email from <rt-link href="https://www.fandango.com/about-us" target="_blank" data-qa="about-fandango-link" style="--textColor: var(--blueLink);">Fandango Media Brands</rt-link>. </rt-text></login-check-email-screen></template><template slot="screens" id="login-error-screen"><login-error-screen data-qa="login-error"><rt-text slot="header" size="1.5" context="heading" data-qa="header">Something went wrong... </rt-text><rt-text slot="description1" size="1" context="label" data-qa="description1">Please try again. </rt-text><img slot="image" src="/assets/pizza-pie/images/icons/cognito-error.c55e509a7fd.svg" /><rt-text hidden slot="description2" size="1" context="label" data-qa="description2"></rt-text><rt-link slot="ctaLink" hidden context="label" size="0.875" data-qa="retry-link">Retry</rt-link></login-error-screen></template><template slot="screens" id="login-enter-password-screen"><login-enter-password-screen data-qa="login-enter-password-screen"><rt-text slot="title" size="1.5" style="--fontWeight: var(--franklinGothicMedium);">Welcome back! </rt-text><rt-text slot="username" data-qa="user-email">username@email.com </rt-text><input-label slot="inputPassword" state="default" data-qa="password-input-label"><label slot="label" for="pass">Password</label><input slot="input" id="pass" type="password" placeholder="Password" autocomplete="off" data-qa="password-input"></input></input-label><rt-button disabled slot="continueButton" type="cta-large" data-qa="continue-btn">Continue </rt-button><rt-button slot="emailLoginButton" theme="light" shape="pill" data-qa="send-email-btn">Send email to verify </rt-button><rt-link slot="forgotPasswordLink" theme="light" data-qa="forgot-password-link">Forgot password</rt-link><rt-text class="terms-and-policies" slot="termsAndPolicies" size="0.75">By continuing, you agree to the <rt-link href="/policies/terms-and-policies" target="_blank" data-qa="terms-policies-link" style="--textColor: var(--blueLink);">Terms and Policies</rt-link>and the <rt-link href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" data-qa="privacy-policy-link" style="--textColor: var(--blueLink);">Privacy Policy</rt-link>and to receive email from <rt-link href="https://www.fandango.com/about-us" target="_blank" data-qa="about-fandango-link" style="--textColor: var(--blueLink);">Fandango Media Brands</rt-link>. </rt-text></login-enter-password-screen></template><template slot="screens" id="login-start-screen"><login-start-screen data-qa="login-start-screen"><input-label slot="inputEmail" state="default" data-qa="email-input-label"><label slot="label" for="login-email-input">Email address</label><input slot="input" autocomplete="username" id="login-email-input" placeholder="Email address" type="text" data-qa="email-input" /></input-label><rt-button disabled slot="emailLoginButton" type="cta-large" data-qa="continue-btn">Continue </rt-button><rt-button slot="googleLoginButton" shape="pill" theme="light" style="--buttonHeight: 52px; --borderRadius: 32px;" data-qa="google-login-btn" data-type="google"><div class="social-login-btn-content"><img height="16px" width="16px" src="/assets/pizza-pie/images/vendor/google/google_logo.28d9eb28faa.svg" />Continue with Google </div></rt-button><rt-button slot="appleLoginButton" shape="pill" theme="light" style="--buttonHeight: 52px; --borderRadius: 32px;" data-qa="apple-login-btn" data-type="apple"><div class="social-login-btn-content"><rt-icon size="1" icon="apple"></rt-icon>Continue with apple </div></rt-button><rt-link slot="resetLink" class="reset-link" context="label" size="0.875" href="/reset-client" data-qa="reset-link">Having trouble logging in? </rt-link><rt-text class="terms-and-policies" slot="termsAndPolicies" size="0.75">By continuing, you agree to the <rt-link href="/policies/terms-and-policies" target="_blank" data-qa="terms-policies-link" style="--textColor: var(--blueLink);">Terms and Policies</rt-link>and the <rt-link href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" data-qa="privacy-policy-link" style="--textColor: var(--blueLink);">Privacy Policy</rt-link>and to receive email from <rt-link href="https://www.fandango.com/about-us" target="_blank" data-qa="about-fandango-link" style="--textColor: var(--blueLink);">Fandango Media Brands</rt-link>. </rt-text></login-start-screen></template><template slot="screens" id="login-success-screen"><login-success-screen data-qa="login-success-screen"><rt-text slot="status" size="1.5">Login successful! </rt-text><img slot="icon" src="/assets/pizza-pie/images/icons/cognito-success.3a3d5f32fab.svg" width="111" height="105" /></login-success-screen></template><template slot="screens" id="cognito-opt-in-us"><auth-optin-screen data-qa="auth-opt-in-screen"><div slot="newsletter-text"><h2 class="cognito-optin-form__header unset">Let's keep in touch!</h2></div><img slot="image" class="image" src="https://images.fandango.com/cms/assets/97c33f00-313f-11ee-9aaf-6762c75465cf--newsletter.png" alt="Rotten Tomatoes Newsletter"><h2 slot="sub-title" class="subTitle unset">Sign up for the Rotten Tomatoes newsletter to get weekly updates on: </h2><ul slot="options"><li class="icon-item">Upcoming Movies and TV shows</li><li class="icon-item">Rotten Tomatoes Podcast</li><li class="icon-item">Media News + More</li></ul><rt-button slot="opt-in-button" data-qa="auth-opt-in-screen-opt-in-btn">Sign me up </rt-button><rt-button slot="opt-out-button" class="button--outline" data-qa="auth-opt-in-screen-opt-out-btn">No thanks </rt-button><p slot="foot-note">By clicking "Sign Me Up," you are agreeing to receive occasional emails and communications from Fandango Media (Fandango, Vudu, and Rotten Tomatoes) and consenting to Fandango's <a href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" class="optin-link" target="_blank" rel="noopener" data-qa="auth-name-screen-privacy-policy-link">Privacy Policy</a>and <a href="/policies/terms-and-policies" class="optin-link" target="_blank" rel="noopener" data-qa="auth-name-screen-privacy-policy-link">Terms and Policies</a>. Please allow 10 business days for your account to reflect your preferences. </p></auth-optin-screen></template><template slot="screens" id="cognito-opt-in-foreign"><auth-optin-screen data-qa="auth-opt-in-screen"><div slot="newsletter-text"><h2 class="cognito-optin-form__header unset">Let's keep in touch!</h2></div><img slot="image" class="image" src="https://images.fandango.com/cms/assets/97c33f00-313f-11ee-9aaf-6762c75465cf--newsletter.png" alt="Rotten Tomatoes Newsletter"><h2 slot="sub-title" class="subTitle unset">Sign up for the Rotten Tomatoes newsletter to get weekly updates on: </h2><ul slot="options"><li class="icon-item">Upcoming Movies and TV shows</li><li class="icon-item">Rotten Tomatoes Podcast</li><li class="icon-item">Media News + More</li></ul><rt-button slot="opt-in-button" data-qa="auth-opt-in-screen-opt-in-btn">Sign me up </rt-button><rt-button slot="opt-out-button" class="button--outline" data-qa="auth-opt-in-screen-opt-out-btn">No thanks </rt-button></auth-optin-screen></template><template slot="screens" id="cognito-opt-in-success"><auth-verify-screen><rt-icon icon="check-circled" slot="icon"></rt-icon><p class="h3" slot="status">OK, got it!</p></auth-verify-screen></template></div><div id="emptyPlaceholder"></div><script ASYNC src="//assets.adobedtm.com/launch-EN549327edc13e414a9beb5d61bfd9aac6.min.js"></script><div id="main" class="container rt-layout__body"><a href="#main-page-content" class="skip-link">Skip to Main Content</a><div id="header_and_leaderboard"><div id="top_leaderboard_wrapper" class="leaderboard_wrapper "><ad-unit hidden unit-display="desktop" unit-type="topbanner" adjust-height><div slot="ad-inject"></div></ad-unit><ad-unit hidden unit-display="mobile" unit-type="mbanner"><div slot="ad-inject"></div></ad-unit></div></div><rt-header-manager></rt-header-manager><rt-header aria-label="navigation bar" class="navbar" data-qa="header-nav-bar" data-AdsGlobalNavTakeoverManager="header" id="header-main" skeleton="panel"><button aria-label="Open aRTi" class="arti-mobile" data-ArtiManager="btnArti:click" slot="arti-mobile"><img alt="arti" src="/assets/pizza-pie/images/arti.041d204c4a4.svg" /></button><div slot="mobile-header-nav"><rt-button id="mobile-header-nav-btn" data-RtHeaderManager="mobileHeaderNavBtn:click" size="1.6" style="--backgroundColor: transparent; --backgroundColorHover: transparent; --buttonPadding: 0 10px 4px;">&#9776; </rt-button><mobile-header-nav id="mobile-header-nav" data-RtHeaderManager="mobileHeaderNav"><rt-img slot="logoImage" alt="Rotten Tomatoes" fetchpriority="high" src="/assets/pizza-pie/images/rt-tomato-logo.20c3bdbc97b.svg"></rt-img><div slot="menusCss"></div><div slot="menus"></div></mobile-header-nav></div><a class="logo-wrap" data-AdsGlobalNavTakeoverManager="logoLink" data-SearchResultsNavManager="rtNavLogo" href="/" id="navbar" slot="logo"><img alt="Rotten Tomatoes" data-qa="header-logo" data-AdsGlobalNavTakeoverManager="logo" src="/assets/pizza-pie/images/rtlogo.9b892cff3fd.png" fetchpriority="high" /><div class="hide"><ad-unit hidden unit-display="desktop,mobile" unit-type="logorepeat" unit-targeting="ploc=rtlogo;"><div slot="ad-inject"></div></ad-unit></div></a><search-results-nav-manager></search-results-nav-manager><search-results-nav data-adobe-id="global-nav-search" data-SearchResultsNavManager="search" slot="search" skeleton="chip"><search-results-controls data-SearchResultsNavManager="searchControls" slot="controls"><input aria-label="Search" data-AdsGlobalNavTakeoverManager="searchInput" data-SearchResultsNavManager="inputText:click,input,keydown" data-qa="search-input" placeholder="Search" slot="search-input" type="text" /><rt-button class="search-clear" data-qa="search-clear" data-AdsGlobalNavTakeoverManager="searchClearBtn" data-SearchResultsNavManager="clearBtn:click" size="0.875" slot="search-clear" theme="transparent"><rt-icon icon="close"></rt-icon></rt-button><rt-link class="search-submit" aria-label="Submit search" data-qa="search-submit" data-AdsGlobalNavTakeoverManager="searchSubmitBtn" data-SearchResultsNavManager="submitBtn:click" href="/search" size="0.875" slot="search-submit"><rt-icon icon="search"></rt-icon></rt-link><rt-button class="search-cancel" data-qa="search-cancel" data-AdsGlobalNavTakeoverManager="searchCancelBtn" data-SearchResultsNavManager="cancelBtn:click" size="0.875" slot="search-cancel" theme="transparent">Cancel </rt-button></search-results-controls><search-results aria-expanded="false" class="hide" data-SearchResultsNavManager="searchResults" slot="results"></search-results></search-results-nav><ul slot="nav-links"><li><a href="/about" data-qa="header:link-whats-tmeter" data-AdsGlobalNavTakeoverManager="text">About Rotten Tomatoes&reg; </a></li><li><a href="/critics" data-qa="header:link-critics-home" data-AdsGlobalNavTakeoverManager="text">Critics </a></li><li data-RtHeaderManager="loginLink"><ul><li><button id="masthead-show-login-btn" class="js-cognito-signin button--link" data-AuthInitiateManager="btnSignIn:click" data-qa="header:login-btn" data-AdsGlobalNavTakeoverManager="text">Login/signup </button></li></ul></li><li class="hide" data-RtHeaderManager="userItem:keydown,keyup,mouseenter" data-qa="header:user"><a class="masthead-user-link" data-RtHeaderManager="navUserlink:focus" rel="nofollow" data-qa="user-profile-link"><img data-RtHeaderManager="navUserImg" data-qa="user-profile-thumb"><p data-AdsGlobalNavTakeoverManager="text" data-RtHeaderManager="navUserFirstName" data-qa="user-profile-name"></p><rt-icon data-AdsGlobalNavTakeoverManager="text" icon="down-dir" image></rt-icon></a><rt-header-user-info class="hide" data-RtHeaderManager="userInfo:focusout,mouseleave"><a data-qa="user-stats-profile-pic" href="" rel="nofollow" slot="imageExpanded" tabindex="-1"><img src="" width="40" alt=""></a><a slot="fullName" rel="nofollow" href="" class="username" data-qa="user-stats-name"></a><a slot="wts" rel="nofollow" href="" class="wts-count-block" data-qa="user-stats-wts"><rt-icon icon="plus" data-qa="user-stats-ratings-count"></rt-icon><span class="count" data-qa="user-stats-wts-count"></span>&nbsp;Wants to See </a><a slot="rating" rel="nofollow" href="" class="rating-count-block" data-qa="user-stats-ratings"><rt-icon icon="star" data-qa="user-stats-ratings-count"></rt-icon><span class="count"></span>&nbsp;Ratings </a><a slot="profileLink" rel="nofollow" class="dropdown-link" href="" data-qa="user-stats-profile-link">Profile</a><a slot="accountLink" rel="nofollow" class="dropdown-link" href="/user/account" data-qa="user-stats-account-link">Account</a><a slot="logoutLink" class="dropdown-link" data-RtHeaderManager="logoutLink:click" href="#logout" data-qa="user-stats-logout-link">Log Out</a></rt-header-user-info></li></ul><rt-header-nav slot="nav-dropdowns"><button aria-label="Open aRTi" class="arti-desktop" data-ArtiManager="btnArti:click" slot="arti-desktop"><img alt="arti" src="/assets/pizza-pie/images/arti.041d204c4a4.svg" /></button><rt-header-nav-item slot="movies" data-qa="masthead:movies-dvds"><a class="unset" slot="link" href="/browse/movies_in_theaters/sort:popular" data-qa="masthead:movies-dvds-link" data-AdsGlobalNavTakeoverManager="text">Movies </a><rt-header-nav-item-dropdown aria-expanded="false" slot="dropdown" data-qa="movies-menu"><rt-header-nav-item-dropdown-list slot="column" data-qa="header-movies-in-theaters"><p slot="title" class="h4" data-qa="movies-in-theaters-main-link"><a class="unset" href="/browse/movies_in_theaters/sort:popular">Movies in theaters</a></p><ul slot="links"><li data-qa="in-theaters-item"><a href="/browse/movies_in_theaters/sort:newest" data-qa="opening-this-week-link">Opening This Week</a></li><li data-qa="in-theaters-item"><a href="/browse/movies_in_theaters/sort:top_box_office" data-qa="top-box-office-link">Top Box Office</a></li><li data-qa="in-theaters-item"><a href="/browse/movies_coming_soon/" data-qa="coming-soon-link">Coming Soon to Theaters</a></li><li data-qa="in-theaters-item"><a href="/browse/movies_in_theaters/critics:certified_fresh~sort:popular" data-qa="certified-fresh-link">Certified Fresh Movies</a></li></ul></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-on-dvd-streaming"><p slot="title" class="h4" data-qa="dvd-streaming-main-link"><a class="unset" href="/browse/movies_at_home">Movies at Home</a></p><ul slot="links"><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/affiliates:fandango-at-home" data-qa="fandango-at-home-link">Fandango at Home</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/affiliates:peacock" data-qa="peacock-link">Peacock</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/affiliates:netflix" data-qa="netflix-link">Netflix</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/affiliates:apple-tv-plus" data-qa="apple-tv-link">Apple TV+</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/affiliates:prime-video" data-qa="prime-video-link">Prime Video</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/sort:popular" data-qa="most-popular-streaming-movies-link">Most Popular Streaming movies</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/critics:certified_fresh" data-qa="certified-fresh-movies-link">Certified Fresh movies</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home" data-qa="browse-all-link">Browse all</a></li></ul></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-movies-more"><p slot="title" class="h4">More</p><ul slot="links"><li data-qa="what-to-watch-item"><a href="https://editorial.rottentomatoes.com/rt-hub/what-to-watch" class="what-to-watch" data-qa="what-to-watch-link">What to Watch<rt-badge>New</rt-badge></a></li></ul></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" cfp><p slot="title" class="h4">Certified fresh picks</p><ul slot="links" class="cfp-wrap" data-qa="header-certified-fresh-picks" data-curation="rt-nav-list-cf-picks"><li data-qa="cert-fresh-item"><a class="cfp-tile" href="/m/twinless" data-qa="cert-fresh-link"><tile-dynamic data-qa="tile"><rt-img alt="Twinless poster image" slot="image" src="https://resizing.flixster.com/j7lw2KeY9_XyfZQdqRZGku7_9C8=/206x305/v2/https://resizing.flixster.com/uxoeWz7uWmeYIV94_SzEV_osqe4=/fit-in/180x240/v2/https://resizing.flixster.com/VlylB3xT2RIYmRivMx37O3yD76Q=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2ZlNDQ1MGQ5LTFjN2QtNDIwNC04NWE1LTM5NGM4N2U5ZTgzYy5qcGc=" loading="lazy"></rt-img><div slot="caption" data-track="scores"><div class="score-wrap"><score-icon-critics certified sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" size="1" context="label">98%</rt-text></div><span class="p--small">Twinless</span><span class="sr-only">Link to Twinless</span></div></tile-dynamic></a></li><li data-qa="cert-fresh-item"><a class="cfp-tile" href="/m/hamilton_2020" data-qa="cert-fresh-link"><tile-dynamic data-qa="tile"><rt-img alt="Hamilton poster image" slot="image" src="https://resizing.flixster.com/1woquJmQfEhWCZtm7GcH0NMHsYA=/206x305/v2/https://resizing.flixster.com/PeAJ5ZpF5qB98ZiX6ixNDCgW2P0=/fit-in/180x240/v2/https://resizing.flixster.com/VmBvlTk8-z7pQvDZXTgSdj93WDE=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzkzY2IxZjFkLTE1NjEtNDQ4Yi05NDY3LTcxNzFmMDVhMDczNi5qcGc=" loading="lazy"></rt-img><div slot="caption" data-track="scores"><div class="score-wrap"><score-icon-critics certified sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" size="1" context="label">98%</rt-text></div><span class="p--small">Hamilton</span><span class="sr-only">Link to Hamilton</span></div></tile-dynamic></a></li><li data-qa="cert-fresh-item"><a class="cfp-tile" href="/m/the_thursday_murder_club" data-qa="cert-fresh-link"><tile-dynamic data-qa="tile"><rt-img alt="The Thursday Murder Club poster image" slot="image" src="https://resizing.flixster.com/jeeldFGcfSMgG09ey5VB7TCFiek=/206x305/v2/https://resizing.flixster.com/9LXDkCzIBBNEiPURkB9t6VefF5Q=/fit-in/180x240/v2/https://resizing.flixster.com/rwdeR5xIiN0k7SWr6yXdnmb6zP8=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2EzYWFkZWJiLWE5N2MtNDc3MS1iMDRlLTk0YWVlYzI5M2UxZS5qcGc=" loading="lazy"></rt-img><div slot="caption" data-track="scores"><div class="score-wrap"><score-icon-critics certified sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" size="1" context="label">76%</rt-text></div><span class="p--small">The Thursday Murder Club</span><span class="sr-only">Link to The Thursday Murder Club</span></div></tile-dynamic></a></li></ul></rt-header-nav-item-dropdown-list></rt-header-nav-item-dropdown></rt-header-nav-item><rt-header-nav-item slot="tv" data-qa="masthead:tv"><a class="unset" slot="link" href="/browse/tv_series_browse/sort:popular" data-qa="masthead:tv-link" data-AdsGlobalNavTakeoverManager="text">Tv shows </a><rt-header-nav-item-dropdown aria-expanded="false" slot="dropdown" data-qa="tv-menu"><rt-header-nav-item-dropdown-list slot="column" data-qa="header-tv-list1"><p slot="title" class="h4" data-curation="rt-hp-text-list-3">New TV Tonight </p><ul slot="links" class="score-list-wrap"><li data-qa="list-item"><a class="score-list-item" href="/tv/task/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">89%</rt-text></div><span>Task: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/the_walking_dead_daryl_dixon/s03" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">80%</rt-text></div><span>The Walking Dead: Daryl Dixon: Season 3 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/the_crow_girl/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">80%</rt-text></div><span>The Crow Girl: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/only_murders_in_the_building/s05" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>Only Murders in the Building: Season 5 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/the_girlfriend/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>The Girlfriend: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/aka_charlie_sheen/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>aka Charlie Sheen: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/wizards_beyond_waverly_place/s02" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>Wizards Beyond Waverly Place: Season 2 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/seen_and_heard_the_history_of_black_television/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>Seen &amp; Heard: the History of Black Television: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/the_fragrant_flower_blooms_with_dignity/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>The Fragrant Flower Blooms With Dignity: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/guts_and_glory/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>Guts &amp; Glory: Season 1 </span></a></li></ul><a class="a--short" data-qa="tv-list1-view-all-link" href="/browse/tv_series_browse/sort:newest" slot="view-all-link">View All </a></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-tv-list2"><p slot="title" class="h4" data-curation="rt-hp-text-list-2">Most Popular TV on RT </p><ul slot="links" class="score-list-wrap"><li data-qa="list-item"><a class="score-list-item" href="/tv/the_paper_2025/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">83%</rt-text></div><span>The Paper: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/dexter_resurrection/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">95%</rt-text></div><span>Dexter: Resurrection: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/alien_earth/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">95%</rt-text></div><span>Alien: Earth: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/task/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">89%</rt-text></div><span>Task: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/wednesday/s02" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">87%</rt-text></div><span>Wednesday: Season 2 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/peacemaker_2022/s02" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">99%</rt-text></div><span>Peacemaker: Season 2 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/the_terminal_list_dark_wolf/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">73%</rt-text></div><span>The Terminal List: Dark Wolf: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/hostage_2025/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">82%</rt-text></div><span>Hostage: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/chief_of_war/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">93%</rt-text></div><span>Chief of War: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/irish_blood/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">100%</rt-text></div><span>Irish Blood: Season 1 </span></a></li></ul><a class="a--short" data-qa="tv-list2-view-all-link" href="/browse/tv_series_browse/sort:popular?" slot="view-all-link">View All </a></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-tv-more"><p slot="title" class="h4">More</p><ul slot="links"><li><a href="https://editorial.rottentomatoes.com/rt-hub/what-to-watch/" class="what-to-watch" data-qa="what-to-watch-link-tv">What to Watch<rt-badge>New</rt-badge></a></li><li><a href="/browse/tv_series_browse/sort:popular" data-qa="tv-best-link"><span>Best TV Shows</span></a></li><li><a href="/browse/tv_series_browse/sort:popular" data-qa="tv-popular-link"><span>Most Popular TV</span></a></li><li><a href="/browse/tv_series_browse/affiliates:fandango-at-home" data-qa="tv-fandango-at-home-link"><span>Fandango at Home</span></a></li><li><a href="/browse/tv_series_browse/affiliates:peacock" data-qa="tv-peacock-link"><span>Peacock</span></a></li><li><a href="/browse/tv_series_browse/affiliates:paramount-plus" data-qa="tv-paramount-link"><span>Paramount+</span></a></li><li><a href="/browse/tv_series_browse/affiliates:netflix" data-qa="tv-netflix-link"><span>Netflix</span></a></li><li><a href="/browse/tv_series_browse/affiliates:prime-video" data-qa="tv-prime-video-link"><span>Prime Video</span></a></li><li><a href="/browse/tv_series_browse/affiliates:apple-tv-plus" data-qa="tv-apple-tv-plus-link"><span>Apple TV+</span></a></li></ul></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" cfp data-qa="header-certified-fresh-pick"><p slot="title" class="h4">Certified fresh pick </p><ul slot="links" class="cfp-wrap" data-curation="rt-nav-list-cf-picks"><li><a class="cfp-tile" href="/tv/the_paper_2025/s01" data-qa="cert-fresh-link"><tile-dynamic data-qa="tile"><rt-img alt="The Paper: Season 1 poster image" slot="image" src="https://resizing.flixster.com/yFijQcjPYUWUelgmiZLHgkXU7hw=/206x305/v2/https://resizing.flixster.com/DFkkHf5pEVX_apKtIQZcoEvI6RU=/fit-in/180x240/v2/https://resizing.flixster.com/texEZJLAG-KcVpfCdkT2R1t4cmE=/ems.cHJkLWVtcy1hc3NldHMvdHZzZWFzb24vYTM3OWM2MTctN2M3Ny00MjdhLTk4NDUtODE5ZWUwMWExNGRhLnBuZw==" loading="lazy"></rt-img><div slot="caption" data-track="scores"><div class="score-wrap"><score-icon-critics certified sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" size="1" context="label">83%</rt-text></div><span class="p--small">The Paper: Season 1</span><span class="sr-only">Link to The Paper: Season 1</span></div></tile-dynamic></a></li></ul></rt-header-nav-item-dropdown-list></rt-header-nav-item-dropdown></rt-header-nav-item><rt-header-nav-item slot="shop"><a class="unset" id="appLink" slot="link" href="https://editorial.rottentomatoes.com/article/app/" target="_blank" data-qa="masthead:app-link" data-AdsGlobalNavTakeoverManager="text">RT App <temporary-display slot="temporary-display" key="app" element="#appLink" event="click"><rt-badge hidden>New</rt-badge></temporary-display></a></rt-header-nav-item><rt-header-nav-item slot="news" data-qa="masthead:news"><a class="unset" slot="link" href="https://editorial.rottentomatoes.com/" data-qa="masthead:news-link" data-AdsGlobalNavTakeoverManager="text">News </a><rt-header-nav-item-dropdown aria-expanded="false" slot="dropdown" data-qa="news-menu"><rt-header-nav-item-dropdown-list slot="column" data-qa="header-news-columns"><p slot="title" class="h4">Columns</p><ul slot="links"><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/all-time-lists/" data-pageheader="All-Time Lists" data-qa="column-link">All-Time Lists </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/binge-guide/" data-pageheader="Binge Guide" data-qa="column-link">Binge Guide </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/comics-on-tv/" data-pageheader="Comics on TV" data-qa="column-link">Comics on TV </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/countdown/" data-pageheader="Countdown" data-qa="column-link">Countdown </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/five-favorite-films/" data-pageheader="Five Favorite Films" data-qa="column-link">Five Favorite Films </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/video-interviews/" data-pageheader="Video Interviews" data-qa="column-link">Video Interviews </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/weekend-box-office/" data-pageheader="Weekend Box Office" data-qa="column-link">Weekend Box Office </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/weekly-ketchup/" data-pageheader="Weekly Ketchup" data-qa="column-link">Weekly Ketchup </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/what-to-watch/" data-pageheader="What to Watch" data-qa="column-link">What to Watch </a></li></ul></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-news-guides"><p slot="title" class="h4">Guides</p><ul slot="links" class="news-wrap"><li data-qa="guides-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/guide/best-football-movies/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="59 Best Football Movies, Ranked by Tomatometer poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2023/09/600EssentialFootballMovies.png" loading="lazy"></rt-img><div slot="caption"><p>59 Best Football Movies, Ranked by Tomatometer</p><span class="sr-only">Link to 59 Best Football Movies, Ranked by Tomatometer</span></div></tile-dynamic></a></li><li data-qa="guides-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/guide/best-new-rom-coms-romance-movies/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="50 Best New Rom-Coms and Romance Movies poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2025/08/Best_New_Romcoms600.jpg" loading="lazy"></rt-img><div slot="caption"><p>50 Best New Rom-Coms and Romance Movies</p><span class="sr-only">Link to 50 Best New Rom-Coms and Romance Movies</span></div></tile-dynamic></a></li></ul><a class="a--short" data-qa="guides-view-all-link" href="https://editorial.rottentomatoes.com/countdown/" slot="view-all-link">View All </a></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-news-hubs"><p slot="title" class="h4">Hubs</p><ul slot="links" class="news-wrap"><li data-qa="hubs-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/rt-hub/what-to-watch/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="What to Watch: In Theaters and On Streaming poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2023/05/RT_WTW_Generic_2023_Thumbnail_600x314_021623.jpg" loading="lazy"></rt-img><div slot="caption"><p>What to Watch: In Theaters and On Streaming</p><span class="sr-only">Link to What to Watch: In Theaters and On Streaming</span></div></tile-dynamic></a></li><li data-qa="hubs-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/rt-hub/awards-tour/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="Awards Tour poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2023/02/RT_AwardsTour_Thumbnail_600x314.jpg" loading="lazy"></rt-img><div slot="caption"><p>Awards Tour</p><span class="sr-only">Link to Awards Tour</span></div></tile-dynamic></a></li></ul><a class="a--short" data-qa="hubs-view-all-link" href="https://editorial.rottentomatoes.com/rt-hubs/" slot="view-all-link">View All </a></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-news-rt-news"><p slot="title" class="h4">RT News</p><ul slot="links" class="news-wrap"><li data-qa="rt-news-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/article/new-movies-and-shows-streaming-in-september-2025-what-to-watch-on-netflix-prime-video-hbo-max-disney-and-more/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="New Movies and Shows Streaming in September: What to watch on Netflix, Prime Video, HBO Max, Disney+ and More poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2025/08/New_Streaming_September_2025-Rep.jpg" loading="lazy"></rt-img><div slot="caption"><p>New Movies and Shows Streaming in September: What to watch on Netflix, Prime Video, HBO Max, Disney+ and More</p><span class="sr-only">Link to New Movies and Shows Streaming in September: What to watch on Netflix, Prime Video, HBO Max, Disney+ and More</span></div></tile-dynamic></a></li><li data-qa="rt-news-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/article/the-conjuring-last-rites-first-reviews/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="<em>The Conjuring: Last Rites</em>First Reviews: A Frightful, Fitting Send-off poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2025/09/Conjuring_Last_Rites_Reviews-Rep.jpg" loading="lazy"></rt-img><div slot="caption"><p><em>The Conjuring: Last Rites</em>First Reviews: A Frightful, Fitting Send-off</p><span class="sr-only">Link to <em>The Conjuring: Last Rites</em>First Reviews: A Frightful, Fitting Send-off</span></div></tile-dynamic></a></li></ul><a class="a--short" data-qa="rt-news-view-all-link" href="https://editorial.rottentomatoes.com/news/" slot="view-all-link">View All </a></rt-header-nav-item-dropdown-list></rt-header-nav-item-dropdown></rt-header-nav-item><rt-header-nav-item slot="showtimes"><a class="unset" slot="link" href="https://www.fandango.com/movies-in-theaters?a=13036" target="_blank" rel="noopener" data-qa="masthead:tickets-showtimes-link" data-AdsGlobalNavTakeoverManager="text">Showtimes </a></rt-header-nav-item></rt-header-nav></rt-header><ads-global-nav-takeover-manager></ads-global-nav-takeover-manager><section class="trending-bar"><ad-unit hidden id="trending_bar_ad" unit-display="desktop" unit-type="trendinggraphic"><div slot="ad-inject"></div></ad-unit><div id="trending-bar-start" class="trending-list-wrap" data-qa="trending-bar"><ul class="list-inline trending-bar__list" data-curation="rt-nav-trending" data-qa="trending-bar-list"><li class="trending-bar__header">Trending on RT</li><li><a class="trending-bar__link" href="https://editorial.rottentomatoes.com/article/2025-emmys-ballot-complete-with-tomatometer-and-popcornmeter-scores/" data-qa="trending-bar-item">Emmy Noms </a></li><li><a class="trending-bar__link" href="https://editorial.rottentomatoes.com/article/movie-re-releases-calendar/" data-qa="trending-bar-item">Re-Release Calendar </a></li><li><a class="trending-bar__link" href="https://editorial.rottentomatoes.com/article/renewed-and-cancelled-tv-shows-2025/" data-qa="trending-bar-item">Renewed and Cancelled TV </a></li><li><a class="trending-bar__link" href="https://editorial.rottentomatoes.com/article/app/" data-qa="trending-bar-item">The Rotten Tomatoes App </a></li></ul><div class="trending-bar__social" data-qa="trending-bar-social-list"><social-media-icons theme="light" size="14"></social-media-icons></div></div></section><main id="main_container" class="container rt-layout__content"><div id="main-page-content"><div class="search__container layout"><section class="search__main layout__column layout__column--main"><h1 class="unset"><rt-text context="heading" size="1.625">Search Results for : "peacemaker"</rt-text></h1><search-page-manager searchQuery="peacemaker"></search-page-manager><div id="search-results" data-qa="search-results"><nav class="search__nav" slot="searchNav"><ul class="searchNav__filters"><li class="js-search-filter searchNav__filter searchNav__filter-all searchNav__filter--active" data-filter="all" tabindex="0"><span data-qa="search-filter-text">All</span></li><li class="js-search-filter searchNav__filter" data-filter="movie" tabindex="0"><span data-qa="search-filter-text">Movies (23)</span></li><li class="js-search-filter searchNav__filter" data-filter="tvSeries" tabindex="0"><span data-qa="search-filter-text">TV Shows (7)</span></li><li class="js-search-filter searchNav__filter" data-filter="celebrity" tabindex="0"><span data-qa="search-filter-text">Celebrities (6)</span></li></ul></nav><search-page-result skeleton="panel" type="movie" data-qa="search-result"><h2 class="unset" slot="title" data-qa="search-result-title"><rt-text context="heading" size="1.25">Movies </rt-text></h2><button slot="prev-btn" data-qa="paging-btn-prev">Prev</button><button slot="next-btn" data-qa="paging-btn-next">Next</button><ul slot="list"><search-page-media-row skeleton="panel" cast="George Clooney,Nicole Kidman,Marcel Iures" data-qa="data-row" endyear="" releaseyear="1997" startyear="" tomatometeriscertified="false" tomatometerscore="49" tomatometersentiment="NEGATIVE"><a href="https://www.rottentomatoes.com/m/1079516-peacemaker" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="The Peacemaker" loading="lazy" src="https://resizing.flixster.com/i7uhe_5HmxsFme8g4xpk6rrTpOk=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p19891_p_v11_al.jpg"></a><a href="https://www.rottentomatoes.com/m/1079516-peacemaker" class="unset" data-qa="info-name" slot="title">The Peacemaker </a></search-page-media-row><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="" releaseyear="2023" startyear="" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/m/peacemaker" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Peacemaker" loading="lazy" src="https://resizing.flixster.com/FifmckbmDWBU26ZxH4_lLiS66z4=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p24452284_p_v13_aa.jpg"></a><a href="https://www.rottentomatoes.com/m/peacemaker" class="unset" data-qa="info-name" slot="title">Peacemaker </a></search-page-media-row><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="" releaseyear="2016" startyear="" tomatometeriscertified="false" tomatometerscore="87" tomatometersentiment="POSITIVE"><a href="https://www.rottentomatoes.com/m/the_peacemaker_2018" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="The Peacemaker" loading="lazy" src="https://resizing.flixster.com/57Z1o_MPNtDPHudShZjMopFfQ_o=/fit-in/80x126/v2/https://resizing.flixster.com/87BNtC1RS1AzWpX2mdbXRAZAHiw=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzFkOTkyMTdjLWM4OTMtNDBiMS04OGI2LTZiOGRmYWIwYWNjZS53ZWJw"></a><a href="https://www.rottentomatoes.com/m/the_peacemaker_2018" class="unset" data-qa="info-name" slot="title">The Peacemaker </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Robert Forster,Lance Edwards,Hilary Shepard" data-qa="data-row" endyear="" releaseyear="1990" startyear="" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/m/1016079-peacemaker" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Peacemaker" loading="lazy" src="https://resizing.flixster.com/_tGJbBoysB6Gu0VFvwctHpl6L2Q=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p12586_p_v7_aa.jpg"></a><a href="https://www.rottentomatoes.com/m/1016079-peacemaker" class="unset" data-qa="info-name" slot="title">Peacemaker </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Yuki Kaji,Yumiko Kobayashi,Jรดji Nakata" data-qa="data-row" endyear="" releaseyear="2018" startyear="" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/m/peacemaker_kurogane_belief" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Peacemaker Kurogane: Belief" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="https://www.rottentomatoes.com/m/peacemaker_kurogane_belief" class="unset" data-qa="info-name" slot="title">Peacemaker Kurogane: Belief </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Emeka Ike,Emeka Enyiocha,Angela Phillips" data-qa="data-row" endyear="" releaseyear="2015" startyear="" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/m/uloma_the_peacemaker" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Uloma the Peacemaker" loading="lazy" src="https://resizing.flixster.com/K-Uyv5b-TMkPka8rj_LqyLfZD0U=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p16449680_p_v8_ac.jpg"></a><a href="https://www.rottentomatoes.com/m/uloma_the_peacemaker" class="unset" data-qa="info-name" slot="title">Uloma the Peacemaker </a></search-page-media-row><search-page-media-row skeleton="panel" cast="James Mitchell,Rosemarie Stack,Jan Merlin" data-qa="data-row" endyear="" releaseyear="1956" startyear="" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/m/the_peacemaker" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="The Peacemaker" loading="lazy" src="https://resizing.flixster.com/TYOqAw5VyXeOWOHM6acK5KteyRU=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p2038_p_v8_aa.jpg"></a><a href="https://www.rottentomatoes.com/m/the_peacemaker" class="unset" data-qa="info-name" slot="title">The Peacemaker </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Onny Michael,Frederick Leonard,Beray Macwizu" data-qa="data-row" endyear="" releaseyear="2021" startyear="" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/m/golden_heart" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Golden Heart" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="https://www.rottentomatoes.com/m/golden_heart" class="unset" data-qa="info-name" slot="title">Golden Heart </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Jennifer Eliogu,Kelechi Udegbe,Mary Lazarus" data-qa="data-row" endyear="" releaseyear="2021" startyear="" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/m/sinners_2021" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Sinners" loading="lazy" src="https://resizing.flixster.com/orLFZ-NVwmIUPctXGGUsKZXEPuk=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20810527_v_v9_aa.jpg"></a><a href="https://www.rottentomatoes.com/m/sinners_2021" class="unset" data-qa="info-name" slot="title">Sinners </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Kenneth Okolie,Mary Lazarus" data-qa="data-row" endyear="" releaseyear="2018" startyear="" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/m/i_saw_the_devil" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="I Saw the Devil" loading="lazy" src="https://resizing.flixster.com/Lyl7Iw3_uWEl0CmGZuH_1gM7kSM=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p19057469_v_v8_aa.jpg"></a><a href="https://www.rottentomatoes.com/m/i_saw_the_devil" class="unset" data-qa="info-name" slot="title">I Saw the Devil </a></search-page-media-row></ul><button slot="more-btn" data-qa="search-more-btn">More Movies...</button></search-page-result><search-page-result skeleton="panel" type="tvSeries" data-qa="search-result"><h2 class="unset" slot="title" data-qa="search-result-title"><rt-text context="heading" size="1.25">TV shows </rt-text></h2><button slot="prev-btn" data-qa="paging-btn-prev">Prev</button><button slot="next-btn" data-qa="paging-btn-next">Next</button><ul slot="list"><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="" releaseyear="" startyear="2022" tomatometeriscertified="false" tomatometerscore="96" tomatometersentiment="POSITIVE"><a href="https://www.rottentomatoes.com/tv/peacemaker_2022" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Peacemaker" loading="lazy" src="https://resizing.flixster.com/IvieKvEZLCexNMA3tSUj2dCDIjk=/fit-in/80x126/v2/https://resizing.flixster.com/mEY3rZ4CClCMjpOT2WJSWBvf_p8=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvOTY5OTU0MzYtZmM5MC00MmI4LWI1YTEtYjQ4NDU2MWRmZWMzLmpwZw=="></a><a href="https://www.rottentomatoes.com/tv/peacemaker_2022" class="unset" data-qa="info-name" slot="title">Peacemaker </a></search-page-media-row><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="" releaseyear="" startyear="2020" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/tv/peacemaker" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Peacemaker" loading="lazy" src="https://resizing.flixster.com/D03p_C1KJHhZ4GQ07tzprwv7hQs=/fit-in/80x126/v2/https://resizing.flixster.com/NrEddnxNqQ5ArFwvUWxbcLW2-hU=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvUlRUVjEwMTE0NDkud2VicA=="></a><a href="https://www.rottentomatoes.com/tv/peacemaker" class="unset" data-qa="info-name" slot="title">Peacemaker </a></search-page-media-row><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="" releaseyear="" startyear="2010" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/tv/la_gang_wars_the_peacemaker" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="L.A. Gang Wars: The Peacemaker" loading="lazy" src="https://resizing.flixster.com/VQzveXPV2xFuHoVgjMv99xuw7gE=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p8393398_b_v13_ai.jpg"></a><a href="https://www.rottentomatoes.com/tv/la_gang_wars_the_peacemaker" class="unset" data-qa="info-name" slot="title">L.A. Gang Wars: The Peacemaker </a></search-page-media-row><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="2003" releaseyear="" startyear="2003" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/tv/peacemakers" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Peacemakers" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="https://www.rottentomatoes.com/tv/peacemakers" class="unset" data-qa="info-name" slot="title">Peacemakers </a></search-page-media-row><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="" releaseyear="" startyear="1992" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/tv/pagemaker_40_learning_system" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Pagemaker 4.0 Learning System" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="https://www.rottentomatoes.com/tv/pagemaker_40_learning_system" class="unset" data-qa="info-name" slot="title">Pagemaker 4.0 Learning System </a></search-page-media-row><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="" releaseyear="" startyear="" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/tv/pagemaker_30_learning_system" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Pagemaker 3.0 Learning System" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="https://www.rottentomatoes.com/tv/pagemaker_30_learning_system" class="unset" data-qa="info-name" slot="title">Pagemaker 3.0 Learning System </a></search-page-media-row><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="" releaseyear="" startyear="2006" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/tv/the_facemakers" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="The Facemakers" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="https://www.rottentomatoes.com/tv/the_facemakers" class="unset" data-qa="info-name" slot="title">The Facemakers </a></search-page-media-row></ul><button slot="more-btn" data-qa="search-more-btn">More TV Shows...</button></search-page-result><ad-unit hidden unit-display="mobile" unit-type="interscroller" no-retry><aside slot="ad-inject" class="center mobile-interscroller"></aside></ad-unit><search-page-result skeleton="panel" type="celebrity" data-qa="search-result"><h2 class="unset" slot="title" data-qa="search-result-title"><rt-text context="heading" size="1.25">Celebrities </rt-text></h2><button slot="prev-btn" data-qa="paging-btn-prev">Prev</button><button slot="next-btn" data-qa="paging-btn-next">Next</button><ul slot="list"><search-page-item-row skeleton="panel" data-qa="data-row"><a href="/celebrity/peacemaker_unamba" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Peacemaker Unamba" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="/celebrity/peacemaker_unamba" class="unset" data-qa="info-name" slot="title"><span>Peacemaker Unamba</span></a></search-page-item-row><search-page-item-row skeleton="panel" data-qa="data-row"><a href="/celebrity/peacemaker_simon" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Peacemaker Simon" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="/celebrity/peacemaker_simon" class="unset" data-qa="info-name" slot="title"><span>Peacemaker Simon</span></a></search-page-item-row><search-page-item-row skeleton="panel" data-qa="data-row"><a href="/celebrity/simon_peacemaker" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Simon Peacemaker" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="/celebrity/simon_peacemaker" class="unset" data-qa="info-name" slot="title"><span>Simon Peacemaker</span></a></search-page-item-row><search-page-item-row skeleton="panel" data-qa="data-row"><a href="/celebrity/simeon_peacemaker" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Simeon Peacemaker" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="/celebrity/simeon_peacemaker" class="unset" data-qa="info-name" slot="title"><span>Simeon Peacemaker</span></a></search-page-item-row><search-page-item-row skeleton="panel" data-qa="data-row"><a href="/celebrity/simon_peacemaker_onyimba" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Simon Peacemaker Onyimba" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="/celebrity/simon_peacemaker_onyimba" class="unset" data-qa="info-name" slot="title"><span>Simon Peacemaker Onyimba</span></a></search-page-item-row><search-page-item-row skeleton="panel" data-qa="data-row"><a href="/celebrity/770727094" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Gerry &amp; The Pacemakers" loading="lazy" src="https://resizing.flixster.com/tSp76M6rhz9cdyt6bxhAAL43QIA=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/v9/AllPhotos/449176/449176_v9_ba.jpg"></a><a href="/celebrity/770727094" class="unset" data-qa="info-name" slot="title"><span>Gerry &amp; The Pacemakers</span></a></search-page-item-row></ul><button slot="more-btn" data-qa="search-more-btn">More Celebrities...</button></search-page-result></div></section><section class="search__sidebar layout__column layout__column--sidebar"><div class="adColumn__content"><ad-unit hidden unit-display="desktop" unit-type="topmulti" show-ad-link><div slot="ad-inject"></div></ad-unit></div></section></div></div><back-to-top hidden></back-to-top></main><ad-unit hidden unit-display="desktop" unit-type="bottombanner"><div slot="ad-inject" class="sleaderboard_wrapper"></div></ad-unit><ads-global-skin-takeover-manager></ads-global-skin-takeover-manager><footer-manager></footer-manager><footer class="footer container" data-PagePicturesManager="footer"><mobile-app-desktop-footer env="production" hidden></mobile-app-desktop-footer><div class="footer__content-desktop-block" data-qa="footer:section"><div class="footer__content-group"><ul class="footer__links-list"><li class="footer__links-list-item"><a href="/help_desk" data-qa="footer:link-helpdesk">Help</a></li><li class="footer__links-list-item"><a href="/about" data-qa="footer:link-about">About Rotten Tomatoes</a></li><li id="footer-feedback" class="footer__links-list-item" data-qa="footer-feedback-desktop"></li></ul></div><div class="footer__content-group"><ul class="footer__links-list"><li class="footer__links-list-item"><a href="/critics/criteria" data-qa="footer:link-critic-submission">Critic Submission</a></li><li class="footer__links-list-item"><a href="/help_desk/licensing" data-qa="footer:link-licensing">Licensing</a></li><li class="footer__links-list-item"><a href="https://together.nbcuni.com/advertise/?utm_source=rotten_tomatoes&amp;utm_medium=referral&amp;utm_campaign=property_ad_pages&amp;utm_content=footer" target="_blank" rel="noopener" data-qa="footer:link-ads">Advertise With Us</a></li><li class="footer__links-list-item"><a href="//www.fandango.com/careers" target="_blank" rel="noopener" data-qa="footer:link-careers">Careers</a></li></ul></div><div class="footer__content-group footer__newsletter-block"><p class="h3 footer__content-group-title"><rt-icon icon="mail" size="1.25" style="fill:#fff"></rt-icon>&ensp;Join the Newsletter </p><p class="footer__newsletter-copy">Get the freshest reviews, news, and more delivered right to your inbox!</p><rt-button shape="pill" data-FooterManager="btnNewsLetter:click" data-qa="footer-newsletter-desktop">Join The Newsletter </rt-button><a data-FooterManager="linkNewsLetter" class="button footer__newsletter-btn hide" target="_blank" rel="noopener">Join The Newsletter </a></div><div class="footer__content-group footer__social-block" data-qa="footer:social"><p class="h3 footer__content-group-title">Follow Us</p><social-media-icons theme="light" size="20"></social-media-icons></div></div><div class="footer__content-mobile-block" data-qa="mfooter:section"><div class="footer__content-group"><div class="mobile-app-cta-wrap"><mobile-app-cta env="production" showandroid="false" showios="true" hidden></mobile-app-cta></div><p class="footer__copyright-legal" data-qa="mfooter:copyright"><rt-text size="0.75">Copyright &copy; Fandango. All rights reserved.</rt-text></p><p><rt-button shape="pill" data-FooterManager="btnNewsLetter:click" data-qa="footer-newsletter-mobile">Join The Newsletter</rt-button></p><a data-FooterManager="linkNewsLetter" class="button footer__newsletter-btn hide" target="_blank" rel="noopener">Join The Newsletter</a><ul class="footer__links-list list-inline"><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" rel="noopener" data-qa="mfooter:link-privacy-policy">Privacy Policy </a></li><li class="footer__links-list-item"><a href="/policies/terms-and-policies" data-qa="mfooter:link-terms-policies">Terms and Policies</a></li><li class="footer__links-list-item"><img data-FooterManager="iconCCPA" src="https://images.fandango.com/cms/assets/266533e0-7afb-11ed-83f2-4f600722b564--privacyoptions.svg" class="footer__ccpa-icon" loading="lazy" alt="CCPA icon" /><a href="javascript:void(0)" id="ot-sdk-btn" class="ot-sdk-show-settings mobile" data-qa="footer-cookie-settings-mobile">Cookie Settings</a></li><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/privacy/california-consumer-privacy-act" target="_blank" rel="noopener" data-qa="mfooter:link-california-notice">California Notice</a></li><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/privacy/cookies#accordionheader2" target="_blank" rel="noopener" data-qa="mfooter:link-adChoices">Ad Choices</a></li><li id="footer-feedback-mobile" class="footer__links-list-item" data-qa="footer-feedback-mobile"></li><li class="footer__links-list-item"><a href="/faq#accessibility" data-qa="mfooter:link-accessibility">Accessibility</a></li></ul></div></div><div class="footer__copyright"><ul class="footer__links-list list-inline list-inline--separator" data-qa="footer:links-list-privacy"><li class="footer__links-list-item version" data-qa="footer:version"><span>V3.1</span></li><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" rel="noopener" data-qa="footer:link-privacy-policy">Privacy Policy </a></li><li class="footer__links-list-item"><a href="/policies/terms-and-policies" data-qa="footer:link-terms-policies">Terms and Policies</a></li><li class="footer__links-list-item"><img data-FooterManager="iconCCPA" src="https://images.fandango.com/cms/assets/266533e0-7afb-11ed-83f2-4f600722b564--privacyoptions.svg" class="footer__ccpa-icon" loading="lazy" alt="CCPA icon" /><a href="javascript:void(0)" id="ot-sdk-btn" class="ot-sdk-show-settings" data-qa="footer-cookie-settings-desktop">Cookie Settings</a></li><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/privacy/california-consumer-privacy-act" target="_blank" rel="noopener" data-qa="footer:link-california-notice">California Notice</a></li><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/privacy/cookies#accordionheader2" target="_blank" rel="noopener" data-qa="footer:link-adChoices">Ad Choices</a></li><li class="footer__links-list-item"><a href="/faq#accessibility" data-qa="footer:link-accessibility">Accessibility</a></li></ul><span class="footer__copyright-legal" data-qa="footer:copyright">Copyright &copy; Fandango. A Division of <a href="https://www.nbcuniversal.com" target="_blank" rel="noopener" data-qa="footer:link-nbcuniversal">NBCUniversal</a>. All rights reserved. </span></div></footer></div><iframe-container hidden data-ArtiManager="iframeContainer:close,resize" data-iframe-src="https://arti.rottentomatoes.com?theme=iframe" theme="widget"><span slot="logo"><img src="/assets/pizza-pie/images/arti.041d204c4a4.svg" alt="Logo"></img><span>beta</span></span><rt-button aria-label="New chat" data-ArtiManager="btnNewChat:click" id="artiNewChatButton" slot="optBtns" theme="transparent" title="New chat"><rt-icon icon="new-chat" size="1.25" image></rt-icon></rt-button></iframe-container><arti-manager></arti-manager><script type="text/javascript">(function (root){ root.RottenTomatoes || (root.RottenTomatoes={}); root.RottenTomatoes.context || (root.RottenTomatoes.context={}); root.RottenTomatoes.context.resetCookies=["AMCVS_8CF467C25245AE3F0A490D4C%40AdobeOrg", "AMCV_8CF467C25245AE3F0A490D4C%40AdobeOrg", "WRIgnore", "WRUIDAWS", "__CT_Data", "__gads", "_admrla", "_awl", "_cs_c", "_cs_id", "_cs_mk", "_cs_s", "_fbp", "_ga", "_gat_gtmTracker", "_gid", "aam_uuid", "akamai_generated_location", "auth_token", "auth_user", "auth_client", "check", "cognito", "fblo_326803741017", "fbm_326803741017", "fbsr_326803741017", "gpv_Page", "id_token", "is_auth", "loginPlatform", "mbox", "notice_behavior", "optimizelyBuckets", "optimizelyEndUserId", "optimizelyPendingLogEvents", "optimizelySegments", "s_cc", "s_dayslastvisit", "s_dayslastvisit_s", "s_invisit", "s_prevPage", "s_sq", "s_vnum", "cognito", "fbm_326803741017", "fbsr_326803741017", "id_token", "JSESSIONID", "QSI_HistorySession", "QSI_SI_8up4dWDOtjAg0hn_intercept", "_ALGOLIA", "__Host-color-scheme", "__Host-theme-options", "__host_color_scheme", "__host_theme_options", "_cb", "_cb_ls", "_cb_svref", "_chartbeat2", "_chartbeat4", "_chartbeat5", "_sp_id.47f3", "_sp_ses.47f3", "_v__chartbeat3", "adops_master_kvs", "akacd_RTReplatform", "algoliaUT", "cognito", "cl_duid", "fbsr_326803741017", "id_token", "mps_uuid", "session_id", "_admrla", "_awl", "_ga", "_gid", "aam_uuid", "cognito", "fbm_326803741017", "id_token", "_cb", "_cb_ls", "_cb_svref", "_chartbeat2", "adops_master_kvs", "cognito", "id_token", "krg_crb", "krg_uid", "mps_uuid"]; root.Fandango || (root.Fandango={}); root.Fandango.dtmData={ "webVersion": "node", "rtVersion": 3.1, "loggedInStatus": "", "customerId": "", "pageName": "trailers"}; root.RottenTomatoes.criticPage={ "vanity": "jacqueline-andriakos", "type": "movies", "typeDisplayName": "Movie", "totalReviews": "", "criticID": "15412"}; root.RottenTomatoes.context.video={ "file": "https:\u002F\u002Flink.theplatform.com\u002Fs\u002FNGweTC\u002Fmedia\u002FhggLQwXPvYKW?formats=MPEG-DASH+widevine,M3U+appleHlsEncryption,M3U+none,MPEG-DASH+none,MPEG4,MP3", "type": "hls", "description": "Your mission, should you decide to accept it, is to enjoy all the best scenes in the Mission Impossible franchise-- from the tensest scenes to the craziest stunts, watch Ethan Hunt (Tom Cruise) and the rest of the IMF accept some of the most dangerous missions of all time. What was your favorite Mission Impossible scene? Sound off in the comments below!", "image": "https:\u002F\u002Fstatcdn.fandango.com\u002FMPX\u002Fimage\u002FNBCU_Fandango\u002F874\u002F155\u002Fthumb_FF413F40-AAD7-4F37-92E2-03F3C0857CCC.jpg", "isRedBand": false, "mediaid": "2239667267748", "mpxId": "2239667267748", "publicId": "hggLQwXPvYKW", "title": "Movieclips: Mission Impossible's Best Scenes", "default": false, "label": "0", "duration": "32:30", "durationInSeconds": "1950.574", "emsMediaType": "Movie", "emsId": "cd9060e7-5a09-3f24-af38-8d57a821db37", "overviewPageUrl": "\u002Fm\u002Fmission_impossible_rogue_nation", "videoPageUrl": "\u002Fm\u002Fmission_impossible_rogue_nation\u002Fvideos\u002FhggLQwXPvYKW", "videoType": "CLIP", "adobeDataLayer":{ "content":{ "id": "fandango_2239667267748", "length": "1950.574", "type": "vod", "player_name": "jw", "sdk_version": "web: 6.51.0", "channel": "movie", "originator": "paramount pictures", "name": "movieclips: mission impossible's best scenes", "rating": "not adult", "stream_type": "video"}, "media_params":{ "genre": "action, adventure, mystery & thriller", "show_type": 2}}, "comscore":{ "labelmapping": "c3=\"rottentomatoes.com\", ns_st_st=\"Rotten Tomatoes\", ns_st_pu=\"Paramount Pictures\", ns_st_pr=\"Mission: Impossible - Rogue Nation\", ns_st_sn=\"*null\", ns_st_en=\"*null\", ns_st_ge=\"Action,Adventure,Mystery & Thriller\", ns_st_ia=\"0\", ns_st_ce=\"0\", ns_st_ddt=\"2015\", ns_st_tdt=\"2015\""}, "thumbnail": "https:\u002F\u002Fresizing.flixster.com\u002F9dX2bYPEHEy-jggcJR989Ts_CIE=\u002F270x160\u002Fv2\u002Fhttps:\u002F\u002Fstatcdn.fandango.com\u002FMPX\u002Fimage\u002FNBCU_Fandango\u002F874\u002F155\u002Fthumb_FF413F40-AAD7-4F37-92E2-03F3C0857CCC.jpg"}; root.RottenTomatoes.context.videoClipsJson={ "count": 12}; root.RottenTomatoes.context.review={ "mediaType": "movie", "title": "Andhadhun", "emsId": "30ee238d-cdc4-320f-adfa-474133f2390e", "type": "user", "sort": undefined, "reviewsCount": 0, "pageInfo": undefined, "reviewerDefaultImg": "https:\u002F\u002Fimages.fandango.com\u002Fcms\u002Fassets\u002F5b6ff500-1663-11ec-ae31-05a670d2d590--rtactordefault.png", "reviewerDefaultImgWidth": "100"}; root.RottenTomatoes.context.useCursorPagination=true; root.RottenTomatoes.context.verifiedTooltip=undefined; root.RottenTomatoes.context.layout={ "header":{ "movies":{ "moviesAtHome":{ "tarsSlug": "rt-nav-movies-at-home", "linkList": [{ "header": "Fandango at Home", "slug": "fandango-at-home-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Faffiliates:fandango-at-home"},{ "header": "Peacock", "slug": "peacock-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Faffiliates:peacock"},{ "header": "Netflix", "slug": "netflix-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Faffiliates:netflix"},{ "header": "Apple TV+", "slug": "apple-tv-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Faffiliates:apple-tv-plus"},{ "header": "Prime Video", "slug": "prime-video-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Faffiliates:prime-video"},{ "header": "Most Popular Streaming movies", "slug": "most-popular-streaming-movies-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Fsort:popular"},{ "header": "Certified Fresh movies", "slug": "certified-fresh-movies-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Fcritics:certified_fresh"},{ "header": "Browse all", "slug": "browse-all-link", "url": "\u002Fbrowse\u002Fmovies_at_home"}]}}, "editorial":{ "guides":{ "posts": [{ "ID": 161109, "author": 12, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2023\u002F09\u002F600EssentialFootballMovies.png"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fguide\u002Fbest-football-movies\u002F", "status": "publish", "title": "59 Best Football Movies, Ranked by Tomatometer", "type": "guide"},{ "ID": 253470, "author": 12, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2025\u002F08\u002FBest_New_Romcoms600.jpg"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fguide\u002Fbest-new-rom-coms-romance-movies\u002F", "status": "publish", "title": "50 Best New Rom-Coms and Romance Movies", "type": "guide"}], "title": "Guides", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fcountdown\u002F"}, "hubs":{ "posts": [{ "ID": 237626, "author": 12, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2023\u002F05\u002FRT_WTW_Generic_2023_Thumbnail_600x314_021623.jpg"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Frt-hub\u002Fwhat-to-watch\u002F", "status": "publish", "title": "What to Watch: In Theaters and On Streaming", "type": "rt-hub"},{ "ID": 140214, "author": 12, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2023\u002F02\u002FRT_AwardsTour_Thumbnail_600x314.jpg"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Frt-hub\u002Fawards-tour\u002F", "status": "publish", "title": "Awards Tour", "type": "rt-hub"}], "title": "Hubs", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Frt-hubs\u002F"}, "news":{ "posts": [{ "ID": 273082, "author": 79, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2025\u002F08\u002FNew_Streaming_September_2025-Rep.jpg"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002Fnew-movies-and-shows-streaming-in-september-2025-what-to-watch-on-netflix-prime-video-hbo-max-disney-and-more\u002F", "status": "publish", "title": "New Movies and Shows Streaming in September: What to watch on Netflix, Prime Video, HBO Max, Disney+ and More", "type": "article"},{ "ID": 273326, "author": 669, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2025\u002F09\u002FConjuring_Last_Rites_Reviews-Rep.jpg"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002Fthe-conjuring-last-rites-first-reviews\u002F", "status": "publish", "title": "\u003Cem\u003EThe Conjuring: Last Rites\u003C\u002Fem\u003E First Reviews: A Frightful, Fitting Send-off", "type": "article"}], "title": "RT News", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fnews\u002F"}}, "trendingTarsSlug": "rt-nav-trending", "trending": [{ "header": "Emmy Noms", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002F2025-emmys-ballot-complete-with-tomatometer-and-popcornmeter-scores\u002F"},{ "header": "Re-Release Calendar", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002Fmovie-re-releases-calendar\u002F"},{ "header": "Renewed and Cancelled TV", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002Frenewed-and-cancelled-tv-shows-2025\u002F"},{ "header": "The Rotten Tomatoes App ", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002Fapp\u002F"}], "certifiedMedia":{ "certifiedFreshTvSeason":{ "header": null, "media":{ "url": "\u002Ftv\u002Fthe_paper_2025\u002Fs01", "name": "The Paper: Season 1", "score": 83, "posterImg": "https:\u002F\u002Fresizing.flixster.com\u002FyFijQcjPYUWUelgmiZLHgkXU7hw=\u002F206x305\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FDFkkHf5pEVX_apKtIQZcoEvI6RU=\u002Ffit-in\u002F180x240\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FtexEZJLAG-KcVpfCdkT2R1t4cmE=\u002Fems.cHJkLWVtcy1hc3NldHMvdHZzZWFzb24vYTM3OWM2MTctN2M3Ny00MjdhLTk4NDUtODE5ZWUwMWExNGRhLnBuZw=="}, "tarsSlug": "rt-nav-list-cf-picks"}, "certifiedFreshMovieInTheater":{ "header": null, "media":{ "url": "\u002Fm\u002Ftwinless", "name": "Twinless", "score": 98, "posterImg": "https:\u002F\u002Fresizing.flixster.com\u002Fj7lw2KeY9_XyfZQdqRZGku7_9C8=\u002F206x305\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FuxoeWz7uWmeYIV94_SzEV_osqe4=\u002Ffit-in\u002F180x240\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FVlylB3xT2RIYmRivMx37O3yD76Q=\u002Fems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2ZlNDQ1MGQ5LTFjN2QtNDIwNC04NWE1LTM5NGM4N2U5ZTgzYy5qcGc="}}, "certifiedFreshMovieInTheater4":{ "header": null, "media":{ "url": "\u002Fm\u002Fhamilton_2020", "name": "Hamilton", "score": 98, "posterImg": "https:\u002F\u002Fresizing.flixster.com\u002F1woquJmQfEhWCZtm7GcH0NMHsYA=\u002F206x305\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FPeAJ5ZpF5qB98ZiX6ixNDCgW2P0=\u002Ffit-in\u002F180x240\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FVmBvlTk8-z7pQvDZXTgSdj93WDE=\u002Fems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzkzY2IxZjFkLTE1NjEtNDQ4Yi05NDY3LTcxNzFmMDVhMDczNi5qcGc="}}, "certifiedFreshMovieAtHome":{ "header": null, "media":{ "url": "\u002Fm\u002Fthe_thursday_murder_club", "name": "The Thursday Murder Club", "score": 76, "posterImg": "https:\u002F\u002Fresizing.flixster.com\u002FjeeldFGcfSMgG09ey5VB7TCFiek=\u002F206x305\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002F9LXDkCzIBBNEiPURkB9t6VefF5Q=\u002Ffit-in\u002F180x240\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FrwdeR5xIiN0k7SWr6yXdnmb6zP8=\u002Fems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2EzYWFkZWJiLWE5N2MtNDc3MS1iMDRlLTk0YWVlYzI5M2UxZS5qcGc="}}, "tarsSlug": "rt-nav-list-cf-picks"}, "tvLists":{ "newTvTonight":{ "tarsSlug": "rt-hp-text-list-3", "title": "New TV Tonight", "shows": [{ "title": "Task: Season 1", "tomatometer":{ "tomatometer": 89, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Ftask\u002Fs01"},{ "title": "The Walking Dead: Daryl Dixon: Season 3", "tomatometer":{ "tomatometer": 80, "sentiment": "positive", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fthe_walking_dead_daryl_dixon\u002Fs03"},{ "title": "The Crow Girl: Season 1", "tomatometer":{ "tomatometer": 80, "sentiment": "positive", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fthe_crow_girl\u002Fs01"},{ "title": "Only Murders in the Building: Season 5", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fonly_murders_in_the_building\u002Fs05"},{ "title": "The Girlfriend: Season 1", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fthe_girlfriend\u002Fs01"},{ "title": "aka Charlie Sheen: Season 1", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Faka_charlie_sheen\u002Fs01"},{ "title": "Wizards Beyond Waverly Place: Season 2", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fwizards_beyond_waverly_place\u002Fs02"},{ "title": "Seen & Heard: the History of Black Television: Season 1", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fseen_and_heard_the_history_of_black_television\u002Fs01"},{ "title": "The Fragrant Flower Blooms With Dignity: Season 1", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fthe_fragrant_flower_blooms_with_dignity\u002Fs01"},{ "title": "Guts & Glory: Season 1", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fguts_and_glory\u002Fs01"}]}, "mostPopularTvOnRt":{ "tarsSlug": "rt-hp-text-list-2", "title": "Most Popular TV on RT", "shows": [{ "title": "The Paper: Season 1", "tomatometer":{ "tomatometer": 83, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fthe_paper_2025\u002Fs01"},{ "title": "Dexter: Resurrection: Season 1", "tomatometer":{ "tomatometer": 95, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fdexter_resurrection\u002Fs01"},{ "title": "Alien: Earth: Season 1", "tomatometer":{ "tomatometer": 95, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Falien_earth\u002Fs01"},{ "title": "Task: Season 1", "tomatometer":{ "tomatometer": 89, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Ftask\u002Fs01"},{ "title": "Wednesday: Season 2", "tomatometer":{ "tomatometer": 87, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fwednesday\u002Fs02"},{ "title": "Peacemaker: Season 2", "tomatometer":{ "tomatometer": 99, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fpeacemaker_2022\u002Fs02"},{ "title": "The Terminal List: Dark Wolf: Season 1", "tomatometer":{ "tomatometer": 73, "sentiment": "positive", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fthe_terminal_list_dark_wolf\u002Fs01"},{ "title": "Hostage: Season 1", "tomatometer":{ "tomatometer": 82, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fhostage_2025\u002Fs01"},{ "title": "Chief of War: Season 1", "tomatometer":{ "tomatometer": 93, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fchief_of_war\u002Fs01"},{ "title": "Irish Blood: Season 1", "tomatometer":{ "tomatometer": 100, "sentiment": "positive", "certified": false}, "tvPageUrl": "\u002Ftv\u002Firish_blood\u002Fs01"}]}}}, "links":{ "moviesInTheaters":{ "certifiedFresh": "\u002Fbrowse\u002Fmovies_in_theaters\u002Fcritics:certified_fresh~sort:popular", "comingSoon": "\u002Fbrowse\u002Fmovies_coming_soon\u002F", "openingThisWeek": "\u002Fbrowse\u002Fmovies_in_theaters\u002Fsort:newest", "title": "\u002Fbrowse\u002Fmovies_in_theaters", "topBoxOffice": "\u002Fbrowse\u002Fmovies_in_theaters"}, "onDvdAndStreaming":{ "all": "\u002Fbrowse\u002Fmovies_at_home\u002F", "certifiedFresh": "\u002Fbrowse\u002Fmovies_at_home\u002Fcritics:certified_fresh", "title": "\u002Fbrowse\u002Fmovies_at_home\u002F", "top": "\u002Fbrowse\u002Fmovies_at_home\u002Fsort:popular"}, "moreMovies":{ "topMovies": "\u002Fbrowse\u002Fmovies_at_home\u002Fsort:popular", "trailers": "\u002Ftrailers"}, "tvTonight": "\u002Fbrowse\u002Ftv_series_browse\u002Fsort:newest", "tvPopular": "\u002Fbrowse\u002Ftv_series_browse\u002Fsort:popular", "moreTv":{ "topTv": "\u002Fbrowse\u002Ftv_series_browse\u002Fsort:popular", "certifiedFresh": "\u002Fbrowse\u002Ftv_series_browse\u002Fcritics:fresh"}, "editorial":{ "allTimeLists": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fall-time-lists\u002F", "bingeGuide": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fbinge-guide\u002F", "comicsOnTv": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fcomics-on-tv\u002F", "countdown": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fcountdown\u002F", "fiveFavoriteFilms": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Ffive-favorite-films\u002F", "videoInterviews": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fvideo-interviews\u002F", "weekendBoxOffice": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fweekend-box-office\u002F", "weeklyKetchup": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fweekly-ketchup\u002F", "whatToWatch": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwhat-to-watch\u002F"}, "advertisingFooter": "https:\u002F\u002Ftogether.nbcuni.com\u002Fadvertise\u002F?utm_source=rotten_tomatoes&utm_medium=referral&utm_campaign=property_ad_pages&utm_content=footer", "californiaNotice": "https:\u002F\u002Fwww.nbcuniversalprivacy.com\u002Fprivacy\u002Fcalifornia-consumer-privacy-act", "careers": "\u002F\u002Fwww.fandango.com\u002Fcareers", "cookieManagement": "https:\u002F\u002Fwww.nbcuniversalprivacy.com\u002Fprivacy\u002Fcookies#accordionheader2", "fandangoAbout": "https:\u002F\u002Fwww.fandango.com\u002Fabout-us", "privacyPolicy": "https:\u002F\u002Fwww.nbcuniversalprivacy.com\u002Ffandango-privacy-policy", "termsPolicies": "\u002Fpolicies\u002Fterms-and-policies"}}; root.RottenTomatoes.thirdParty={ "chartBeat":{ "auth": "64558", "domain": "rottentomatoes.com"}, "mpx":{ "accountPid": "NGweTC", "playerPid": "y__7B0iQTi4P", "playerPidPDK6": "pdk6_y__7B0iQTi4P", "accountId": "2474312077"}, "algoliaSearch":{ "aId": "79FRDP12PN", "sId": "175588f6e5f8319b27702e4cc4013561"}, "cognito":{ "upId": "us-west-2_4L0ZX4b1U", "clientId": "7pu48v8i2n25t4vhes0edck31c"}}; root.RottenTomatoes.serviceWorker={ "isServiceWokerOn": true}; root.__RT__ || (root.__RT__={}); root.__RT__.featureFlags={ "adsCarouselHP": false, "adsCarouselHPSlug": "rt-sponsored-carousel-list-mcdonalds-hp", "adsCarouselOP": false, "adsCarouselOPSlug": "rt-sponsored-carousel-list-mcdonalds-op", "adsMockDLP": false, "adsPages": "none", "adsSponsoredOverrideOP": true, "adsSponsoredOverrideOPSlugs": "rt-sponsored-override-op-starz", "adsVideoSpotlightHP": false, "appleSigninEnabled": true, "artiEnabled": true, "authPasswordEnabled": true, "authVerboseLogs": false, "bypassCriticValidationEnabled": false, "castAndCrewEnabled": true, "cookieConsentServiceEnabled": false, "crssoEnabled": false, "editorialApiDisabled": false, "faqUpdatesEnabled": true, "legacyBridge": true, "logVerboseEnabled": false, "mobileAppAndroid": "https:\u002F\u002Fplay.google.com\u002Fstore\u002Fapps\u002Fdetails?id=com.rottentomatoes.android", "mobileAppIos": "https:\u002F\u002Fapps.apple.com\u002Fus\u002Fapp\u002Frotten-tomatoes-movies-tv\u002Fid6673916573", "mobileAppIosMeta": "app-id=6673916573, app-argument=https:\u002F\u002Fwww.rottentomatoes.com\u002F", "mobileNavEnabled": true, "oneTrustJwtApiUrl": "https:\u002F\u002Fonetrustjwt.services.fandango.com", "oneTrustJwtServiceEnabled": false, "pageJsonEnabled": false, "profilesFeaturesEnabled": false, "profilesUsernameEnabled": false, "redesignMediaHeroEnabled": true, "redesignMoreLikeThis": true, "redesignSortTable": true, "trafficAndroidEnabled": false, "trafficSafariEnabled": true, "userMigrationEnabled": true, "versantFreewheelEnabled": false, "versantMpsDomain": "app.mps.vsnt.net", "versantMpsEnabled": false, "versantOneTrustScriptBlock": "\u003C!-- OneTrust Cookies Consent Notice start for rottentomatoes.com --\u003E \u003Cscript src=\"https:\u002F\u002Fcdn.cookielaw.org\u002Fconsent\u002F01978557-1604-76a7-ad7c-18216757cf52-test\u002FotSDKStub.js\" type=\"text\u002Fjavascript\" charset=\"UTF-8\" data-domain-script=\"01978557-1604-76a7-ad7c-18216757cf52-test\" integrity=\"sha384-Exfxdyaw5OnsUlHEKlNlz7OwgVCyLlitAtJsDmSNh3LeLlCjWXos3X\u002FCMNUbQ\u002FgA\" crossorigin=\"anonymous\" \u003E\u003C\u002Fscript\u003E \u003Cscript type=\"text\u002Fjavascript\"\u003E function OptanonWrapper(){ if (OnetrustActiveGroups.includes('7')){ document.querySelector('search-results-nav-manager')?.setAlgoliaInsightUserToken?.();}} \u003C\u002Fscript\u003E \u003C!-- OneTrust Cookies Consent Notice end for rottentomatoes.com --\u003E \u003C!-- OneTrust IAB US Privacy (USP) --\u003E \u003Cscript src=\"https:\u002F\u002Fcdn.cookielaw.org\u002Fopt-out\u002FotCCPAiab.js\" id=\"privacyCookie\" type=\"text\u002Fjavascript\" charset=\"UTF-8\" ccpa-opt-out-ids=\"USP\" ccpa-opt-out-geo=\"US\" ccpa-opt-out-lspa=\"false\"\u003E\u003C\u002Fscript\u003E \u003C!-- OneTrust IAB US Privacy (USP) end --\u003E", "videoGeoFencingEnabled": true}; root.RottenTomatoes.context.adsMockDLP=false; root.RottenTomatoes.context.req={ "params":{}, "query":{ "search": "peacemaker"}, "route":{}, "url": "\u002Fsearch?search=peacemaker", "secure": false, "buildVersion": undefined}; root.RottenTomatoes.context.config={}; root.BK={ "PageName": "http:\u002F\u002Fwww.rottentomatoes.com\u002Fm\u002Fnight_swim", "SiteID": 37528, "SiteSection": "movie", "MovieId": "fb5d1291-c772-301c-b3ff-c50dfefaeaaa", "MovieTitle": "Night Swim"}; root.RottenTomatoes.dtmData={ "customerId": "", "loggedInStatus": "", "rtVersion": 3.1, "webVersion": "node", "emsID": "fb5d1291-c772-301c-b3ff-c50dfefaeaaa", "lifeCycleWindow": "OUT_OF_THEATERS", "pageName": "rt | movies | overview | Night Swim", "titleGenre": "Horror", "titleId": "fb5d1291-c772-301c-b3ff-c50dfefaeaaa", "titleName": "Night Swim", "titleType": "Movie"}; root.RottenTomatoes.context.gptSite="search";}(this)); </script><script fetchpriority="high" src="/assets/pizza-pie/javascripts/bundles/roma/preload.18bcfff8e54.js"></script><script src="/assets/pizza-pie/javascripts/bundles/roma/vendors.a4cc402b78a.js"></script><script src="/assets/pizza-pie/javascripts/bundles/roma/default.24dc1977289.js"></script><script async data-SearchResultsNavManager="script:load" src="https://cdn.jsdelivr.net/npm/algoliasearch@4/dist/algoliasearch-lite.umd.js"></script><script src="/assets/pizza-pie/javascripts/templates/roma/searchNav.a3288ea5efe.js"></script><script src="/assets/pizza-pie/javascripts/bundles/roma/searchNav.6a836b4ca81.js"></script><script src="/assets/pizza-pie/javascripts/bundles/search.3cbb7fc9cd1.js"></script><script>if (window.mps && typeof window.mps.writeFooter==='function'){ window.mps.writeFooter();} </script><script>window._satellite && _satellite.pageBottom(); </script></body></html> 1 + <!DOCTYPE html><html lang="en" dir="ltr" xmlns="http://www.w3.org/1999/xhtml" prefix="fb: http://www.facebook.com/2008/fbml og: http://opengraphprotocol.org/schema/"><head prefix="og: http://ogp.me/ns# flixstertomatoes: http://ogp.me/ns/apps/flixstertomatoes#"><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><meta http-equiv="x-ua-compatible" content="ie=edge"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="shortcut icon" sizes="76x76" type="image/x-icon" href="https://www.rottentomatoes.com/assets/pizza-pie/images/favicon.ico" /><title>Search Results | Rotten Tomatoes</title><meta name="description" content="Rotten Tomatoes, home of the Tomatometer, is the most trusted measurement of quality for Movies & TV. The definitive site for Reviews, Trailers, Showtimes, and Tickets"><meta property="fb:app_id" content=""><meta property="og:site_name" content="Rotten Tomatoes"><meta property="og:title" content="Search Results"><meta property="og:description" content="Rotten Tomatoes, home of the Tomatometer, is the most trusted measurement of quality for Movies & TV. The definitive site for Reviews, Trailers, Showtimes, and Tickets"><meta property="og:type" content=""><meta property="og:url" content=""><meta property="og:image" content="https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/RT_TwitterCard_2018.jpg"><meta property="og:locale" content="en_US"><meta name="twitter:card" content="summary_large_image"><meta name="twitter:image" content="https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/RT_TwitterCard_2018.jpg"><meta name="twitter:title" content="Search Results"><meta name="twitter:text:title" content="Search Results"><meta name="twitter:description" content="Rotten Tomatoes, home of the Tomatometer, is the most trusted measurement of quality for Movies & TV. The definitive site for Reviews, Trailers, Showtimes, and Tickets"><meta name="twitter:site" content="@rottentomatoes"></head><body class="body no-touch js-mptd-layout" data-AdsGlobalSkinTakeoverManager="body" data-SearchResultsNavManager="body"><div id="main" class="container rt-layout__body"><rt-header aria-label="navigation bar" class="navbar" data-qa="header-nav-bar" data-AdsGlobalNavTakeoverManager="header" id="header-main" skeleton="panel"></rt-header><section class="trending-bar"></section><main id="main_container" class="container rt-layout__content"><div id="main-page-content"><div class="search__container layout"><section class="search__main layout__column layout__column--main"><div id="search-results" data-qa="search-results"><nav class="search__nav" slot="searchNav"></nav><search-page-result skeleton="panel" type="movie" data-qa="search-result"><h2 class="unset" slot="title" data-qa="search-result-title"><rt-text context="heading" size="1.25">Movies </rt-text></h2><button slot="prev-btn" data-qa="paging-btn-prev">Prev</button><button slot="next-btn" data-qa="paging-btn-next">Next</button><ul slot="list"><search-page-media-row skeleton="panel" cast="George Clooney,Nicole Kidman,Marcel Iures" data-qa="data-row" endyear="" releaseyear="1997" startyear="" tomatometeriscertified="false" tomatometerscore="49" tomatometersentiment="NEGATIVE"><a href="https://www.rottentomatoes.com/m/1079516-peacemaker" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="The Peacemaker" loading="lazy" src="https://resizing.flixster.com/i7uhe_5HmxsFme8g4xpk6rrTpOk=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p19891_p_v11_al.jpg"></a><a href="https://www.rottentomatoes.com/m/1079516-peacemaker" class="unset" data-qa="info-name" slot="title">The Peacemaker </a></search-page-media-row><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="" releaseyear="2023" startyear="" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/m/peacemaker" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Peacemaker" loading="lazy" src="https://resizing.flixster.com/FifmckbmDWBU26ZxH4_lLiS66z4=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p24452284_p_v13_aa.jpg"></a><a href="https://www.rottentomatoes.com/m/peacemaker" class="unset" data-qa="info-name" slot="title">Peacemaker </a></search-page-media-row><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="" releaseyear="2016" startyear="" tomatometeriscertified="false" tomatometerscore="87" tomatometersentiment="POSITIVE"><a href="https://www.rottentomatoes.com/m/the_peacemaker_2018" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="The Peacemaker" loading="lazy" src="https://resizing.flixster.com/57Z1o_MPNtDPHudShZjMopFfQ_o=/fit-in/80x126/v2/https://resizing.flixster.com/87BNtC1RS1AzWpX2mdbXRAZAHiw=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzFkOTkyMTdjLWM4OTMtNDBiMS04OGI2LTZiOGRmYWIwYWNjZS53ZWJw"></a><a href="https://www.rottentomatoes.com/m/the_peacemaker_2018" class="unset" data-qa="info-name" slot="title">The Peacemaker </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Robert Forster,Lance Edwards,Hilary Shepard" data-qa="data-row" endyear="" releaseyear="1990" startyear="" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/m/1016079-peacemaker" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Peacemaker" loading="lazy" src="https://resizing.flixster.com/_tGJbBoysB6Gu0VFvwctHpl6L2Q=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p12586_p_v7_aa.jpg"></a><a href="https://www.rottentomatoes.com/m/1016079-peacemaker" class="unset" data-qa="info-name" slot="title">Peacemaker </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Yuki Kaji,Yumiko Kobayashi,Jรดji Nakata" data-qa="data-row" endyear="" releaseyear="2018" startyear="" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/m/peacemaker_kurogane_belief" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Peacemaker Kurogane: Belief" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="https://www.rottentomatoes.com/m/peacemaker_kurogane_belief" class="unset" data-qa="info-name" slot="title">Peacemaker Kurogane: Belief </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Emeka Ike,Emeka Enyiocha,Angela Phillips" data-qa="data-row" endyear="" releaseyear="2015" startyear="" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/m/uloma_the_peacemaker" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Uloma the Peacemaker" loading="lazy" src="https://resizing.flixster.com/K-Uyv5b-TMkPka8rj_LqyLfZD0U=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p16449680_p_v8_ac.jpg"></a><a href="https://www.rottentomatoes.com/m/uloma_the_peacemaker" class="unset" data-qa="info-name" slot="title">Uloma the Peacemaker </a></search-page-media-row><search-page-media-row skeleton="panel" cast="James Mitchell,Rosemarie Stack,Jan Merlin" data-qa="data-row" endyear="" releaseyear="1956" startyear="" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/m/the_peacemaker" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="The Peacemaker" loading="lazy" src="https://resizing.flixster.com/TYOqAw5VyXeOWOHM6acK5KteyRU=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p2038_p_v8_aa.jpg"></a><a href="https://www.rottentomatoes.com/m/the_peacemaker" class="unset" data-qa="info-name" slot="title">The Peacemaker </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Onny Michael,Frederick Leonard,Beray Macwizu" data-qa="data-row" endyear="" releaseyear="2021" startyear="" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/m/golden_heart" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Golden Heart" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="https://www.rottentomatoes.com/m/golden_heart" class="unset" data-qa="info-name" slot="title">Golden Heart </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Jennifer Eliogu,Kelechi Udegbe,Mary Lazarus" data-qa="data-row" endyear="" releaseyear="2021" startyear="" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/m/sinners_2021" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Sinners" loading="lazy" src="https://resizing.flixster.com/orLFZ-NVwmIUPctXGGUsKZXEPuk=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20810527_v_v9_aa.jpg"></a><a href="https://www.rottentomatoes.com/m/sinners_2021" class="unset" data-qa="info-name" slot="title">Sinners </a></search-page-media-row><search-page-media-row skeleton="panel" cast="Kenneth Okolie,Mary Lazarus" data-qa="data-row" endyear="" releaseyear="2018" startyear="" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/m/i_saw_the_devil" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="I Saw the Devil" loading="lazy" src="https://resizing.flixster.com/Lyl7Iw3_uWEl0CmGZuH_1gM7kSM=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p19057469_v_v8_aa.jpg"></a><a href="https://www.rottentomatoes.com/m/i_saw_the_devil" class="unset" data-qa="info-name" slot="title">I Saw the Devil </a></search-page-media-row></ul><button slot="more-btn" data-qa="search-more-btn">More Movies...</button></search-page-result><search-page-result skeleton="panel" type="tvSeries" data-qa="search-result"><h2 class="unset" slot="title" data-qa="search-result-title"><rt-text context="heading" size="1.25">TV shows </rt-text></h2><button slot="prev-btn" data-qa="paging-btn-prev">Prev</button><button slot="next-btn" data-qa="paging-btn-next">Next</button><ul slot="list"><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="" releaseyear="" startyear="2022" tomatometeriscertified="false" tomatometerscore="96" tomatometersentiment="POSITIVE"><a href="https://www.rottentomatoes.com/tv/peacemaker_2022" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Peacemaker" loading="lazy" src="https://resizing.flixster.com/IvieKvEZLCexNMA3tSUj2dCDIjk=/fit-in/80x126/v2/https://resizing.flixster.com/mEY3rZ4CClCMjpOT2WJSWBvf_p8=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvOTY5OTU0MzYtZmM5MC00MmI4LWI1YTEtYjQ4NDU2MWRmZWMzLmpwZw=="></a><a href="https://www.rottentomatoes.com/tv/peacemaker_2022" class="unset" data-qa="info-name" slot="title">Peacemaker </a></search-page-media-row><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="" releaseyear="" startyear="2020" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/tv/peacemaker" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Peacemaker" loading="lazy" src="https://resizing.flixster.com/D03p_C1KJHhZ4GQ07tzprwv7hQs=/fit-in/80x126/v2/https://resizing.flixster.com/NrEddnxNqQ5ArFwvUWxbcLW2-hU=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvUlRUVjEwMTE0NDkud2VicA=="></a><a href="https://www.rottentomatoes.com/tv/peacemaker" class="unset" data-qa="info-name" slot="title">Peacemaker </a></search-page-media-row><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="" releaseyear="" startyear="2010" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/tv/la_gang_wars_the_peacemaker" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="L.A. Gang Wars: The Peacemaker" loading="lazy" src="https://resizing.flixster.com/VQzveXPV2xFuHoVgjMv99xuw7gE=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p8393398_b_v13_ai.jpg"></a><a href="https://www.rottentomatoes.com/tv/la_gang_wars_the_peacemaker" class="unset" data-qa="info-name" slot="title">L.A. Gang Wars: The Peacemaker </a></search-page-media-row><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="2003" releaseyear="" startyear="2003" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/tv/peacemakers" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Peacemakers" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="https://www.rottentomatoes.com/tv/peacemakers" class="unset" data-qa="info-name" slot="title">Peacemakers </a></search-page-media-row><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="" releaseyear="" startyear="1992" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/tv/pagemaker_40_learning_system" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Pagemaker 4.0 Learning System" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="https://www.rottentomatoes.com/tv/pagemaker_40_learning_system" class="unset" data-qa="info-name" slot="title">Pagemaker 4.0 Learning System </a></search-page-media-row><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="" releaseyear="" startyear="" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/tv/pagemaker_30_learning_system" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Pagemaker 3.0 Learning System" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="https://www.rottentomatoes.com/tv/pagemaker_30_learning_system" class="unset" data-qa="info-name" slot="title">Pagemaker 3.0 Learning System </a></search-page-media-row><search-page-media-row skeleton="panel" cast="" data-qa="data-row" endyear="" releaseyear="" startyear="2006" tomatometeriscertified="false" tomatometerscore="" tomatometersentiment=""><a href="https://www.rottentomatoes.com/tv/the_facemakers" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="The Facemakers" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="https://www.rottentomatoes.com/tv/the_facemakers" class="unset" data-qa="info-name" slot="title">The Facemakers </a></search-page-media-row></ul><button slot="more-btn" data-qa="search-more-btn">More TV Shows...</button></search-page-result><search-page-result skeleton="panel" type="celebrity" data-qa="search-result"><h2 class="unset" slot="title" data-qa="search-result-title"><rt-text context="heading" size="1.25">Celebrities </rt-text></h2><button slot="prev-btn" data-qa="paging-btn-prev">Prev</button><button slot="next-btn" data-qa="paging-btn-next">Next</button><ul slot="list"><search-page-item-row skeleton="panel" data-qa="data-row"><a href="/celebrity/peacemaker_unamba" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Peacemaker Unamba" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="/celebrity/peacemaker_unamba" class="unset" data-qa="info-name" slot="title"><span>Peacemaker Unamba</span></a></search-page-item-row><search-page-item-row skeleton="panel" data-qa="data-row"><a href="/celebrity/peacemaker_simon" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Peacemaker Simon" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="/celebrity/peacemaker_simon" class="unset" data-qa="info-name" slot="title"><span>Peacemaker Simon</span></a></search-page-item-row><search-page-item-row skeleton="panel" data-qa="data-row"><a href="/celebrity/simon_peacemaker" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Simon Peacemaker" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="/celebrity/simon_peacemaker" class="unset" data-qa="info-name" slot="title"><span>Simon Peacemaker</span></a></search-page-item-row><search-page-item-row skeleton="panel" data-qa="data-row"><a href="/celebrity/simeon_peacemaker" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Simeon Peacemaker" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="/celebrity/simeon_peacemaker" class="unset" data-qa="info-name" slot="title"><span>Simeon Peacemaker</span></a></search-page-item-row><search-page-item-row skeleton="panel" data-qa="data-row"><a href="/celebrity/simon_peacemaker_onyimba" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Simon Peacemaker Onyimba" loading="lazy" src="https://images.fandango.com/cms/assets/5d84d010-59b1-11ea-b175-791e911be53d--rt-poster-defaultgif.gif"></a><a href="/celebrity/simon_peacemaker_onyimba" class="unset" data-qa="info-name" slot="title"><span>Simon Peacemaker Onyimba</span></a></search-page-item-row><search-page-item-row skeleton="panel" data-qa="data-row"><a href="/celebrity/770727094" class="unset" data-qa="thumbnail-link" slot="thumbnail"><img alt="Gerry &amp; The Pacemakers" loading="lazy" src="https://resizing.flixster.com/tSp76M6rhz9cdyt6bxhAAL43QIA=/fit-in/80x126/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/v9/AllPhotos/449176/449176_v9_ba.jpg"></a><a href="/celebrity/770727094" class="unset" data-qa="info-name" slot="title"><span>Gerry &amp; The Pacemakers</span></a></search-page-item-row></ul><button slot="more-btn" data-qa="search-more-btn">More Celebrities...</button></search-page-result></div></section></div></div></main><footer class="footer container" data-PagePicturesManager="footer"></footer></div><script type="text/javascript"></script></body></html>
+1 -1
internal/services/samples/series_overview.html
··· 1 - <!DOCTYPE html><html lang="en" dir="ltr" xmlns="http://www.w3.org/1999/xhtml" prefix="fb: http://www.facebook.com/2008/fbml og: http://opengraphprotocol.org/schema/"><head prefix="og: http://ogp.me/ns# flixstertomatoes: http://ogp.me/ns/apps/flixstertomatoes#"><script charset="UTF-8" crossorigin="anonymous" data-domain-script="7e979733-6841-4fce-9182-515fac69187f" integrity="sha384-TKdmlzVmoD70HzftTw4WtOzIBL5mNx8mXSRzEvwrWjpIJ7FZ/EuX758yMDWXtRUN" src="https://cdn.cookielaw.org/consent/7e979733-6841-4fce-9182-515fac69187f/otSDKStub.js" type="text/javascript"></script><script type="text/javascript">function OptanonWrapper(){ if (OnetrustActiveGroups.includes('7')){ document.querySelector('search-results-nav-manager')?.setAlgoliaInsightUserToken?.();}} </script><script ccpa-opt-out-ids="USP" ccpa-opt-out-geo="US" ccpa-opt-out-lspa="false" charset="UTF-8" src="https://cdn.cookielaw.org/opt-out/otCCPAiab.js" type="text/javascript"></script><script src="/assets/pizza-pie/javascripts/bundles/roma/rt-common.js?single"></script><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><meta http-equiv="x-ua-compatible" content="ie=edge"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="shortcut icon" sizes="76x76" type="image/x-icon" href="https://www.rottentomatoes.com/assets/pizza-pie/images/favicon.ico" /><title>Peacemaker (2022) | Rotten Tomatoes</title><meta name="description" content="Discover reviews, ratings, and trailers for Peacemaker (2022) on Rotten Tomatoes. Stay updated with critic and audience scores today!" /><meta name="twitter:card" content="summary" /><meta name="twitter:image" content="https://resizing.flixster.com/UHglta_RX5_h8fsHS2BljZkvfZk=/206x305/v2/https://resizing.flixster.com/mEY3rZ4CClCMjpOT2WJSWBvf_p8=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvOTY5OTU0MzYtZmM5MC00MmI4LWI1YTEtYjQ4NDU2MWRmZWMzLmpwZw==" /><meta name="twitter:title" content="Peacemaker (2022) | Rotten Tomatoes" /><meta name="twitter:text:title" content="Peacemaker (2022) | Rotten Tomatoes" /><meta name="twitter:description" content="Discover reviews, ratings, and trailers for Peacemaker (2022) on Rotten Tomatoes. Stay updated with critic and audience scores today!" /><meta property="og:site_name" content="Rotten Tomatoes" /><meta property="og:title" content="Peacemaker (2022) | Rotten Tomatoes" /><meta property="og:description" content="Discover reviews, ratings, and trailers for Peacemaker (2022) on Rotten Tomatoes. Stay updated with critic and audience scores today!" /><meta property="og:type" content="video.tv_show" /><meta property="og:url" content="https://www.rottentomatoes.com/tv/peacemaker_2022" /><meta property="og:image" content="https://resizing.flixster.com/UHglta_RX5_h8fsHS2BljZkvfZk=/206x305/v2/https://resizing.flixster.com/mEY3rZ4CClCMjpOT2WJSWBvf_p8=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvOTY5OTU0MzYtZmM5MC00MmI4LWI1YTEtYjQ4NDU2MWRmZWMzLmpwZw==" /><meta property="og:locale" content="en_US" /><link rel="canonical" href="https://www.rottentomatoes.com/tv/peacemaker_2022" /><script>var dataLayer=dataLayer || []; var RottenTomatoes=RottenTomatoes ||{}; RottenTomatoes.dtmData={ "customerId": "", "loggedInStatus": "", "rtVersion": 3.1, "webVersion": "node", "emsID": "c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed", "Series Title": "Peacemaker (2022)", "pageName": "rt | tv | series | Peacemaker (2022)", "titleGenre": "Comedy", "titleId": "c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed", "titleName": "Peacemaker (2022)", "titleType": "Tv"}; dataLayer.push({ "customerId": "", "loggedInStatus": "", "rtVersion": 3.1, "webVersion": "node", "emsID": "c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed", "Series Title": "Peacemaker (2022)", "pageName": "rt | tv | series | Peacemaker (2022)", "titleGenre": "Comedy", "titleId": "c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed", "titleName": "Peacemaker (2022)", "titleType": "Tv"}); </script><script id="mps-page-integration">window.mpscall={ "cag[certified_fresh]": "0", "cag[fresh_rotten]": "rotten", "cag[genre]": "Comedy|Action", "cag[release]": "Jan 13, 2022", "cag[movieshow]": "Peacemaker", "cag[score]": "null", "cag[urlid]": "/peacemaker_2022", "cat": "tv|series", "field[env]": "production", "field[rtid]": "c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed", "title": "Peacemaker (2022)", "type": "series", "site": "rottentomatoes-web"}; var mpsopts={ 'host': 'mps.nbcuni.com', 'updatecorrelator': 1}; var mps=mps ||{}; mps._ext=mps._ext ||{}; mps._adsheld=[]; mps._queue=mps._queue ||{}; mps._queue.mpsloaded=mps._queue.mpsloaded || []; mps._queue.mpsinit=mps._queue.mpsinit || []; mps._queue.gptloaded=mps._queue.gptloaded || []; mps._queue.adload=mps._queue.adload || []; mps._queue.adclone=mps._queue.adclone || []; mps._queue.adview=mps._queue.adview || []; mps._queue.refreshads=mps._queue.refreshads || []; mps.__timer=Date.now || function (){ return +new Date}; mps.__intcode="v2"; if (typeof mps.getAd !="function") mps.getAd=function (adunit){ if (typeof adunit !="string") return false; var slotid="mps-getad-" + adunit.replace(/\W/g, ""); if (!mps._ext || !mps._ext.loaded){ mps._queue.gptloaded.push(function (){ typeof mps._gptfirst=="function" && mps._gptfirst(adunit, slotid); mps.insertAd("#" + slotid, adunit)}); mps._adsheld.push(adunit)} return '<div id="' + slotid + '" class="mps-wrapper" data-mps-fill-slot="' + adunit + '"></div>'}; </script><script src="//mps.nbcuni.com/fetch/ext/load-rottentomatoes-web.js?nowrite=2" id="mps-load"></script><script type="application/ld+json">{"@context":"http://schema.org","@type":"TVSeries","actor":[{"@type":"Person","name":"John Cena","sameAs":"https://www.rottentomatoes.com/celebrity/john_cena","image":"https://resizing.flixster.com/qFr2ZK1qYDkqSmM5eT3nz_n6E_g=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/487578_v9_ba.jpg"},{"@type":"Person","name":"Danielle Brooks","sameAs":"https://www.rottentomatoes.com/celebrity/danielle_brooks","image":"https://resizing.flixster.com/KhnY5vsfjM0vtw0cZL3aNxXbeUE=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/765589_v9_bc.jpg"},{"@type":"Person","name":"Freddie Stroma","sameAs":"https://www.rottentomatoes.com/celebrity/freddie_stroma","image":"https://resizing.flixster.com/Yk2eiDCtamfmNlK-xMa7nmEw_Po=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/GNLZZGG00283ZZD.jpg"},{"@type":"Person","name":"Chukwudi Iwuji","sameAs":"https://www.rottentomatoes.com/celebrity/chukwudi_iwuji","image":"https://resizing.flixster.com/uNAFlG9dNMjJwyMbPDiCsbjkX8I=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/565157_v9_ba.jpg"},{"@type":"Person","name":"Jennifer Holland","sameAs":"https://www.rottentomatoes.com/celebrity/jennifer_holland","image":"https://resizing.flixster.com/-xeYAf0O7fGIQHRx_YkL7vnaMMg=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/331642_v9_bb.jpg"},{"@type":"Person","name":"Steve Agee","sameAs":"https://www.rottentomatoes.com/celebrity/steve_agee","image":"https://resizing.flixster.com/YprPSg0SXNIqq-Wy4UEz4ovBnOw=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/223358_v9_bd.jpg"}],"aggregateRating":{"@type":"AggregateRating","bestRating":"100","description":"The Tomatometer rating โ€“ based on the published opinions of hundreds of film and television critics โ€“ is a trusted measurement of movie and TV programming quality for millions of moviegoers. It represents the percentage of professional critic reviews that are positive for a given film or television show.","name":"Tomatometer","ratingCount":150,"ratingValue":"96","reviewCount":150,"worstRating":"0"},"containsSeason":[{"@type":"TVSeason","name":"Season 2","url":"https://www.rottentomatoes.com/tv/peacemaker_2022/s02"},{"@type":"TVSeason","name":"Season 1","url":"https://www.rottentomatoes.com/tv/peacemaker_2022/s01"}],"contentRating":"TV-MA","dateCreated":"2022-01-13","description":"Discover reviews, ratings, and trailers for Peacemaker (2022) on Rotten Tomatoes. Stay updated with critic and audience scores today!","genre":["Comedy","Action"],"image":"https://resizing.flixster.com/mEY3rZ4CClCMjpOT2WJSWBvf_p8=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvOTY5OTU0MzYtZmM5MC00MmI4LWI1YTEtYjQ4NDU2MWRmZWMzLmpwZw==","name":"Peacemaker (2022)","numberOfSeasons":2,"partOfSeries":{"@type":"TVSeries","name":"Peacemaker (2022)","startDate":"2022-01-13","url":"https://www.rottentomatoes.com/tv/peacemaker_2022"},"producer":[{"@type":"Person","name":"James Gunn","sameAs":"https://www.rottentomatoes.com/celebrity/james_gunn","image":"https://images.fandango.com/cms/assets/b0cefeb0-b6a8-11ed-81d8-51a487a38835--poster-default-thumbnail.jpg"},{"@type":"Person","name":"Peter Safran","sameAs":"https://www.rottentomatoes.com/celebrity/peter_safran","image":"https://images.fandango.com/cms/assets/b0cefeb0-b6a8-11ed-81d8-51a487a38835--poster-default-thumbnail.jpg"}],"url":"https://www.rottentomatoes.com/tv/peacemaker_2022","video":{"@type":"VideoObject","thumbnailUrl":"https://statcdn.fandango.com/MPX/image/NBCU_Fandango/166/403/thumb_47CFBBB8-95A3-4634-8B02-581F2C757D98.jpg","name":"Peacemaker: Season 2 Trailer - Weeks Ahead","duration":"1:39","sourceOrganization":"MPX","uploadDate":"2025-08-28T16:36:57","description":"","contentUrl":"https://www.rottentomatoes.com/tv/peacemaker_2022/videos/nTePljVEct61"}}</script><link rel="manifest" href="https://www.rottentomatoes.com/assets/pizza-pie/manifest/manifest.json" /><link rel="apple-touch-icon" href="https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/apple-touch-icon-60.jpg" /><link rel="apple-touch-icon" sizes="152x152" href="https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/apple-touch-icon-152.jpg" /><link rel="apple-touch-icon" sizes="167x167" href="https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/apple-touch-icon-167.jpg" /><link rel="apple-touch-icon" sizes="180x180" href="https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/apple-touch-icon-180.jpg" /><meta name="apple-itunes-app" content="app-id=6673916573, app-argument=https://www.rottentomatoes.com/"><meta name="google-site-verification" content="VPPXtECgUUeuATBacnqnCm4ydGO99reF-xgNklSbNbc" /><meta name="msvalidate.01" content="034F16304017CA7DCF45D43850915323" /><meta name="theme-color" content="#FA320A"><meta http-equiv="x-dns-prefetch-control" content="on"><link rel="dns-prefetch" href="//www.rottentomatoes.com" /><link rel="preconnect" href="//www.rottentomatoes.com" /><link rel="stylesheet" href="/assets/pizza-pie/stylesheets/bundles/layouts/default.b6077682d41.css" /><link rel="preload" href="/assets/pizza-pie/stylesheets/bundles/pages/tvSeries.ab02677a549.css" as="style" onload="this.onload=null;this.rel='stylesheet'" /><script>window.RottenTomatoes={}; window.RTLocals={}; window.nunjucksPrecompiled={}; window.__RT__={}; </script><script src="https://cdn.jwplayer.com/libraries/U8MHzHHR.js"></script><script src="https://sb.scorecardresearch.com/c2/plugins/streamingtag_plugin_jwplayer.js"></script><script>!function (e){ var n="https://s.go-mpulse.net/boomerang/"; if ("False"=="True") e.BOOMR_config=e.BOOMR_config ||{}, e.BOOMR_config.PageParams=e.BOOMR_config.PageParams ||{}, e.BOOMR_config.PageParams.pci=!0, n="https://s2.go-mpulse.net/boomerang/"; if (window.BOOMR_API_key="4RDDZ-2Z6GP-RRNMC-PYEUL-SK6K9", function (){ function e(){ if (!o){ var e=document.createElement("script"); e.id="boomr-scr-as", e.src=window.BOOMR.url, e.async=!0, i.parentNode.appendChild(e), o=!0}} function t(e){ o=!0; var n, t, a, r, d=document, O=window; if (window.BOOMR.snippetMethod=e ? "if" : "i", t=function (e, n){ var t=d.createElement("script"); t.id=n || "boomr-if-as", t.src=window.BOOMR.url, BOOMR_lstart=(new Date).getTime(), e=e || d.body, e.appendChild(t)}, !window.addEventListener && window.attachEvent && navigator.userAgent.match(/MSIE [67]\./)) return window.BOOMR.snippetMethod="s", void t(i.parentNode, "boomr-async"); a=document.createElement("IFRAME"), a.src="about:blank", a.title="", a.role="presentation", a.loading="eager", r=(a.frameElement || a).style, r.width=0, r.height=0, r.border=0, r.display="none", i.parentNode.appendChild(a); try{ O=a.contentWindow, d=O.document.open()} catch (_){ n=document.domain, a.src="javascript:var d=document.open();d.domain='" + n + "';void(0);", O=a.contentWindow, d=O.document.open()} if (n) d._boomrl=function (){ this.domain=n, t()}, d.write("<bo" + "dy onload='document._boomrl();'>"); else if (O._boomrl=function (){ t()}, O.addEventListener) O.addEventListener("load", O._boomrl, !1); else if (O.attachEvent) O.attachEvent("onload", O._boomrl); d.close()} function a(e){ window.BOOMR_onload=e && e.timeStamp || (new Date).getTime()} if (!window.BOOMR || !window.BOOMR.version && !window.BOOMR.snippetExecuted){ window.BOOMR=window.BOOMR ||{}, window.BOOMR.snippetStart=(new Date).getTime(), window.BOOMR.snippetExecuted=!0, window.BOOMR.snippetVersion=12, window.BOOMR.url=n + "4RDDZ-2Z6GP-RRNMC-PYEUL-SK6K9"; var i=document.currentScript || document.getElementsByTagName("script")[0], o=!1, r=document.createElement("link"); if (r.relList && "function"==typeof r.relList.supports && r.relList.supports("preload") && "as" in r) window.BOOMR.snippetMethod="p", r.href=window.BOOMR.url, r.rel="preload", r.as="script", r.addEventListener("load", e), r.addEventListener("error", function (){ t(!0)}), setTimeout(function (){ if (!o) t(!0)}, 3e3), BOOMR_lstart=(new Date).getTime(), i.parentNode.appendChild(r); else t(!1); if (window.addEventListener) window.addEventListener("load", a, !1); else if (window.attachEvent) window.attachEvent("onload", a)}}(), "".length >0) if (e && "performance" in e && e.performance && "function"==typeof e.performance.setResourceTimingBufferSize) e.performance.setResourceTimingBufferSize(); !function (){ if (BOOMR=e.BOOMR ||{}, BOOMR.plugins=BOOMR.plugins ||{}, !BOOMR.plugins.AK){ var n=""=="true" ? 1 : 0, t="", a="eyd6zaauaeceajqacqcoyaaafful3lzp-f-4780c27a5-clienttons-s.akamaihd.net", i="false"=="true" ? 2 : 1, o={ "ak.v": "39", "ak.cp": "1839344", "ak.ai": parseInt("1226367", 10), "ak.ol": "0", "ak.cr": 18, "ak.ipv": 6, "ak.proto": "h2", "ak.rid": "142d2895", "ak.r": 43883, "ak.a2": n, "ak.m": "dsca", "ak.n": "essl", "ak.bpcip": "2607:ec80:1401:440::", "ak.cport": 62979, "ak.gh": "23.205.103.137", "ak.quicv": "", "ak.tlsv": "tls1.3", "ak.0rtt": "", "ak.0rtt.ed": "", "ak.csrc": "-", "ak.acc": "", "ak.t": "1757261615", "ak.ak": "hOBiQwZUYzCg5VSAfCLimQ==F7ryKYjya435igG8RC/8OtaNbm8Faad5KbpGjrcLFo1JAcFeE+SXg8riCQgHW9HagZShxYG3/SnCxjDb8xoegU/PZaLQc2CyDLxQM9Q67nTexzqN07i7hIb95Bv8ZA/6LTdYsbedlAiCANtUoqcWw/KCxF4EmMiAAwhF4sLkxn40J7umMLTJfMh3Ybs92NnSK92yr88pBBk31ailH7eRDlXUZ3kk2Y8ssPvEbZxAJki70srrGQVPK0Cjnk4SwdWxcY1geNc1aZLsXkZ/4BXyeCmCoayFISRGGsM/h25+qr2i9lfoe5coKptUsEgfSHExKYXEDzPnaGvtXilpuODdzPAwNuy0eSsUGQNLUo98Os6EHxh3QnbJxx/QnDxeH1fv/6eh10V/uUj0erkJu1ZWHl9gDAb9NiEmM0iv0u01VWo=", "ak.pv": "3", "ak.dpoabenc": "", "ak.tf": i}; if ("" !==t) o["ak.ruds"]=t; var r={ i: !1, av: function (n){ var t="http.initiator"; if (n && (!n[t] || "spa_hard"===n[t])) o["ak.feo"]=void 0 !==e.aFeoApplied ? 1 : 0, BOOMR.addVar(o)}, rv: function (){ var e=["ak.bpcip", "ak.cport", "ak.cr", "ak.csrc", "ak.gh", "ak.ipv", "ak.m", "ak.n", "ak.ol", "ak.proto", "ak.quicv", "ak.tlsv", "ak.0rtt", "ak.0rtt.ed", "ak.r", "ak.acc", "ak.t", "ak.tf"]; BOOMR.removeVar(e)}}; BOOMR.plugins.AK={ akVars: o, akDNSPreFetchDomain: a, init: function (){ if (!r.i){ var e=BOOMR.subscribe; e("before_beacon", r.av, null, null), e("onbeacon", r.rv, null, null), r.i=!0} return this}, is_complete: function (){ return !0}}}}()}(window);</script></head><body class="body no-touch js-mptd-layout" data-AdsGlobalSkinTakeoverManager="body" data-SearchResultsNavManager="body"><cookie-manager></cookie-manager><device-inspection-manager endpoint="https://www.rottentomatoes.com/napi/device/inspection"></device-inspection-manager><user-activity-manager profiles-features-enabled="false"></user-activity-manager><user-identity-manager profiles-features-enabled="false"></user-identity-manager><ad-unit-manager></ad-unit-manager><auth-initiate-manager profiles-username-enabled="false" data-ArtiManager="authInitiateManager" data-WatchlistButtonManager="authInitiateManager:createAccount"></auth-initiate-manager><auth-profile-manager data-AuthInitiateManager="authProfileManager"></auth-profile-manager><auth-validation-manager data-AuthInitiateManager="authValidation"></auth-validation-manager><overlay-base data-AuthInitiateManager="overlayBase:close" data-PagePollsIndexManager="authOverlay:close" hidden><overlay-flows data-AuthInitiateManager="overlayFlows" slot="content"><action-icon slot="close" class="auth-overlay__icon-button auth-overlay__icon-button--close" aria-label="Close" data-qa="close-overlay-btn" icon="close"></action-icon></overlay-flows></overlay-base><notification-alert data-AuthInitiateManager="authSuccess" animate hidden><rt-icon icon="check-circled"></rt-icon><span>Signed in</span></notification-alert><div id="auth-templates" data-AuthInitiateManager="authTemplates"><template slot="screens" id="account-create-username-screen"><account-create-username-screen data-qa="account-create-username-screen"><input-label slot="input-username" state="default" data-qa="username-input-label"><label slot="label" for="create-username-input">Username</label><input slot="input" id="create-username-input" type="text" placeholder="Username" data-qa="username-input" /></input-label><rt-button disabled slot="btn-continue" shape="pill" data-qa="continue-btn">Continue</rt-button><rt-text class="terms-and-policies" slot="terms-and-policies" size="0.75">By joining, you agree to the <rt-link href="/policies/terms-and-policies" target="_blank" data-qa="terms-policies-link">Terms and Policies</rt-link>and the <rt-link href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" data-qa="privacy-policy-link">Privacy Policy</rt-link>and to receive email from <rt-link href="https://www.fandango.com/about-us" target="_blank" data-qa="about-fandango-link">Fandango Media Brands</rt-link>. </rt-text></account-create-username-screen></template><template slot="screens" id="account-email-change-screen"><account-email-change-screen data-qa="account-email-change-screen" email="user@email.com"><input-label class="new-email-input" state="default" slot="new-email-input" data-qa="email-input-label"><label slot="label" for="newEmail">Enter new email</label><input slot="input" name="newEmail" type="text" placeholder="Enter new email" autocomplete="off" data-qa="email-input"></input></input-label><rt-button slot="submit-button" disabled shape="pill" data-qa="submit-btn">Submit</rt-button><rt-text class="terms-and-policies" slot="terms-and-policies" size="0.75">By joining, you agree to the <rt-link href="/policies/terms-and-policies" target="_blank" data-qa="terms-policies-link">Terms and Policies</rt-link>and <rt-link href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" data-qa="privacy-policy-link">Privacy Policy</rt-link>and to receive email from the <rt-link href="https://www.fandango.com/about-us" target="_blank" data-qa="about-fandango-link">Fandango Media Brands</rt-link>. </rt-text></account-email-change-screen></template><template slot="screens" id="account-email-change-success-screen"><account-email-change-success-screen data-qa="login-create-success-screen"><rt-text slot="message" size="1.5" style="--fontWeight: var(--franklinGothicDemi);">Email change successful</rt-text><rt-text slot="submessage">You are signed out for your security. </br>Please sign in again.</rt-text><img slot="icon" src="/assets/pizza-pie/images/icons/cognito-success.3a3d5f32fab.svg" width="111" height="105" /></account-email-change-success-screen></template><template slot="screens" id="account-password-change-screen"><account-password-change-screen data-qa="account-password-change-screen"><input-label state="default" slot="input-password-existing"><label slot="label" for="password-existing">Existing password</label><input slot="input" name="password-existing" type="password" placeholder="Enter existing password" autocomplete="off"></input></input-label><input-label state="default" slot="input-password-new"><label slot="label" for="password-new">New password</label><input slot="input" name="password-new" type="password" placeholder="Enter new password" autocomplete="off"></input></input-label><rt-button disabled shape="pill" slot="submit-button">Submit</rt-button></account-password-change-screen></template><template slot="screens" id="account-password-change-updating-screen"><login-success-screen data-qa="account-password-change-updating-screen" hidebanner><rt-text slot="status" size="1.5" style="--fontWeight: var(--franklinGothicDemi);">Updating your password... </rt-text><img slot="icon" src="/assets/pizza-pie/images/icons/cognito-success.3a3d5f32fab.svg" width="111" height="105" /></login-success-screen></template><template slot="screens" id="account-password-change-success-screen"><login-success-screen data-qa="account-password-change-success-screen" hidebanner><rt-text slot="status" size="1.5" style="--fontWeight: var(--franklinGothicDemi);">Success! </rt-text><img slot="icon" src="/assets/pizza-pie/images/icons/cognito-success.3a3d5f32fab.svg" width="111" height="105" /></login-success-screen></template><template slot="screens" id="account-verifying-email-screen"><account-verifying-email-screen data-qa="account-verifying-email-screen"><img slot="icon" src="/assets/pizza-pie/images/icons/cognito-auth-verify.e74a69e9a77.svg" alt="email" /><rt-text slot="status">Verifying your email... </rt-text><rt-button type="cta-large" href="/?authFlowScreen=loginStartScreen" slot="retryLink" size="0.875" style="--fontWeight: var(--franklinGothicMedium);" data-qa="retry-link">Retry </rt-button></account-verifying-email-screen></template><template slot="screens" id="cognito-loading"><div><loading-spinner id="cognito-auth-loading-spinner"></loading-spinner><style>#cognito-auth-loading-spinner{ font-size: 2rem; transform: translate(calc(100% - 1em), 250px); width: 50%;} </style></div></template><template slot="screens" id="login-check-email-screen"><login-check-email-screen data-qa="login-check-email-screen" email="user@email.com"><rt-text class="note-text" size="1" slot="noteText">Please open the email link from the same browser you initiated the change email process from. </rt-text><rt-text slot="gotEmailMessage" size="0.875">Didn't you get the email? </rt-text><rt-button slot="resendEmailLink" size="0.875" type="cta-large" data-qa="resend-email-link">Resend email </rt-button><rt-link context="label" slot="troubleLoginLink" size="0.875" href="/reset-client" data-qa="reset-link">Having trouble logging in?</rt-link><rt-text class="terms-and-policies" slot="termsAndPolicies" size="0.75">By continuing, you agree to the <rt-link href="/policies/terms-and-policies" target="_blank" data-qa="terms-policies-link" style="--textColor: var(--blueLink);">Terms and Policies</rt-link>and the <rt-link href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" data-qa="privacy-policy-link" style="--textColor: var(--blueLink);">Privacy Policy</rt-link>and to receive email from <rt-link href="https://www.fandango.com/about-us" target="_blank" data-qa="about-fandango-link" style="--textColor: var(--blueLink);">Fandango Media Brands</rt-link>. </rt-text></login-check-email-screen></template><template slot="screens" id="login-error-screen"><login-error-screen data-qa="login-error"><rt-text slot="header" size="1.5" context="heading" data-qa="header">Something went wrong... </rt-text><rt-text slot="description1" size="1" context="label" data-qa="description1">Please try again. </rt-text><img slot="image" src="/assets/pizza-pie/images/icons/cognito-error.c55e509a7fd.svg" /><rt-text hidden slot="description2" size="1" context="label" data-qa="description2"></rt-text><rt-link slot="ctaLink" hidden context="label" size="0.875" data-qa="retry-link">Retry</rt-link></login-error-screen></template><template slot="screens" id="login-enter-password-screen"><login-enter-password-screen data-qa="login-enter-password-screen"><rt-text slot="title" size="1.5" style="--fontWeight: var(--franklinGothicMedium);">Welcome back! </rt-text><rt-text slot="username" data-qa="user-email">username@email.com </rt-text><input-label slot="inputPassword" state="default" data-qa="password-input-label"><label slot="label" for="pass">Password</label><input slot="input" id="pass" type="password" placeholder="Password" autocomplete="off" data-qa="password-input"></input></input-label><rt-button disabled slot="continueButton" type="cta-large" data-qa="continue-btn">Continue </rt-button><rt-button slot="emailLoginButton" theme="light" shape="pill" data-qa="send-email-btn">Send email to verify </rt-button><rt-link slot="forgotPasswordLink" theme="light" data-qa="forgot-password-link">Forgot password</rt-link><rt-text class="terms-and-policies" slot="termsAndPolicies" size="0.75">By continuing, you agree to the <rt-link href="/policies/terms-and-policies" target="_blank" data-qa="terms-policies-link" style="--textColor: var(--blueLink);">Terms and Policies</rt-link>and the <rt-link href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" data-qa="privacy-policy-link" style="--textColor: var(--blueLink);">Privacy Policy</rt-link>and to receive email from <rt-link href="https://www.fandango.com/about-us" target="_blank" data-qa="about-fandango-link" style="--textColor: var(--blueLink);">Fandango Media Brands</rt-link>. </rt-text></login-enter-password-screen></template><template slot="screens" id="login-start-screen"><login-start-screen data-qa="login-start-screen"><input-label slot="inputEmail" state="default" data-qa="email-input-label"><label slot="label" for="login-email-input">Email address</label><input slot="input" autocomplete="username" id="login-email-input" placeholder="Email address" type="text" data-qa="email-input" /></input-label><rt-button disabled slot="emailLoginButton" type="cta-large" data-qa="continue-btn">Continue </rt-button><rt-button slot="googleLoginButton" shape="pill" theme="light" style="--buttonHeight: 52px; --borderRadius: 32px;" data-qa="google-login-btn" data-type="google"><div class="social-login-btn-content"><img height="16px" width="16px" src="/assets/pizza-pie/images/vendor/google/google_logo.28d9eb28faa.svg" />Continue with Google </div></rt-button><rt-button slot="appleLoginButton" shape="pill" theme="light" style="--buttonHeight: 52px; --borderRadius: 32px;" data-qa="apple-login-btn" data-type="apple"><div class="social-login-btn-content"><rt-icon size="1" icon="apple"></rt-icon>Continue with apple </div></rt-button><rt-link slot="resetLink" class="reset-link" context="label" size="0.875" href="/reset-client" data-qa="reset-link">Having trouble logging in? </rt-link><rt-text class="terms-and-policies" slot="termsAndPolicies" size="0.75">By continuing, you agree to the <rt-link href="/policies/terms-and-policies" target="_blank" data-qa="terms-policies-link" style="--textColor: var(--blueLink);">Terms and Policies</rt-link>and the <rt-link href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" data-qa="privacy-policy-link" style="--textColor: var(--blueLink);">Privacy Policy</rt-link>and to receive email from <rt-link href="https://www.fandango.com/about-us" target="_blank" data-qa="about-fandango-link" style="--textColor: var(--blueLink);">Fandango Media Brands</rt-link>. </rt-text></login-start-screen></template><template slot="screens" id="login-success-screen"><login-success-screen data-qa="login-success-screen"><rt-text slot="status" size="1.5">Login successful! </rt-text><img slot="icon" src="/assets/pizza-pie/images/icons/cognito-success.3a3d5f32fab.svg" width="111" height="105" /></login-success-screen></template><template slot="screens" id="cognito-opt-in-us"><auth-optin-screen data-qa="auth-opt-in-screen"><div slot="newsletter-text"><h2 class="cognito-optin-form__header unset">Let's keep in touch!</h2></div><img slot="image" class="image" src="https://images.fandango.com/cms/assets/97c33f00-313f-11ee-9aaf-6762c75465cf--newsletter.png" alt="Rotten Tomatoes Newsletter"><h2 slot="sub-title" class="subTitle unset">Sign up for the Rotten Tomatoes newsletter to get weekly updates on: </h2><ul slot="options"><li class="icon-item">Upcoming Movies and TV shows</li><li class="icon-item">Rotten Tomatoes Podcast</li><li class="icon-item">Media News + More</li></ul><rt-button slot="opt-in-button" data-qa="auth-opt-in-screen-opt-in-btn">Sign me up </rt-button><rt-button slot="opt-out-button" class="button--outline" data-qa="auth-opt-in-screen-opt-out-btn">No thanks </rt-button><p slot="foot-note">By clicking "Sign Me Up," you are agreeing to receive occasional emails and communications from Fandango Media (Fandango, Vudu, and Rotten Tomatoes) and consenting to Fandango's <a href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" class="optin-link" target="_blank" rel="noopener" data-qa="auth-name-screen-privacy-policy-link">Privacy Policy</a>and <a href="/policies/terms-and-policies" class="optin-link" target="_blank" rel="noopener" data-qa="auth-name-screen-privacy-policy-link">Terms and Policies</a>. Please allow 10 business days for your account to reflect your preferences. </p></auth-optin-screen></template><template slot="screens" id="cognito-opt-in-foreign"><auth-optin-screen data-qa="auth-opt-in-screen"><div slot="newsletter-text"><h2 class="cognito-optin-form__header unset">Let's keep in touch!</h2></div><img slot="image" class="image" src="https://images.fandango.com/cms/assets/97c33f00-313f-11ee-9aaf-6762c75465cf--newsletter.png" alt="Rotten Tomatoes Newsletter"><h2 slot="sub-title" class="subTitle unset">Sign up for the Rotten Tomatoes newsletter to get weekly updates on: </h2><ul slot="options"><li class="icon-item">Upcoming Movies and TV shows</li><li class="icon-item">Rotten Tomatoes Podcast</li><li class="icon-item">Media News + More</li></ul><rt-button slot="opt-in-button" data-qa="auth-opt-in-screen-opt-in-btn">Sign me up </rt-button><rt-button slot="opt-out-button" class="button--outline" data-qa="auth-opt-in-screen-opt-out-btn">No thanks </rt-button></auth-optin-screen></template><template slot="screens" id="cognito-opt-in-success"><auth-verify-screen><rt-icon icon="check-circled" slot="icon"></rt-icon><p class="h3" slot="status">OK, got it!</p></auth-verify-screen></template></div><div id="emptyPlaceholder"></div><script ASYNC src="//assets.adobedtm.com/launch-EN549327edc13e414a9beb5d61bfd9aac6.min.js"></script><div id="main" class="container rt-layout__body"><a href="#main-page-content" class="skip-link">Skip to Main Content</a><div id="header_and_leaderboard"><div id="top_leaderboard_wrapper" class="leaderboard_wrapper "><ad-unit hidden unit-display="desktop" unit-type="topbanner" adjust-height><div slot="ad-inject"></div></ad-unit><ad-unit hidden unit-display="mobile" unit-type="mbanner"><div slot="ad-inject"></div></ad-unit></div></div><rt-header-manager></rt-header-manager><rt-header aria-label="navigation bar" class="navbar" data-qa="header-nav-bar" data-AdsGlobalNavTakeoverManager="header" id="header-main" skeleton="panel"><button aria-label="Open aRTi" class="arti-mobile" data-ArtiManager="btnArti:click" slot="arti-mobile"><img alt="arti" src="/assets/pizza-pie/images/arti.041d204c4a4.svg" /></button><div slot="mobile-header-nav"><rt-button id="mobile-header-nav-btn" data-RtHeaderManager="mobileHeaderNavBtn:click" size="1.6" style="--backgroundColor: transparent; --backgroundColorHover: transparent; --buttonPadding: 0 10px 4px;">&#9776; </rt-button><mobile-header-nav id="mobile-header-nav" data-RtHeaderManager="mobileHeaderNav"><rt-img slot="logoImage" alt="Rotten Tomatoes" fetchpriority="high" src="/assets/pizza-pie/images/rt-tomato-logo.20c3bdbc97b.svg"></rt-img><div slot="menusCss"></div><div slot="menus"></div></mobile-header-nav></div><a class="logo-wrap" data-AdsGlobalNavTakeoverManager="logoLink" data-SearchResultsNavManager="rtNavLogo" href="/" id="navbar" slot="logo"><img alt="Rotten Tomatoes" data-qa="header-logo" data-AdsGlobalNavTakeoverManager="logo" src="/assets/pizza-pie/images/rtlogo.9b892cff3fd.png" fetchpriority="high" /><div class="hide"><ad-unit hidden unit-display="desktop,mobile" unit-type="logorepeat" unit-targeting="ploc=rtlogo;"><div slot="ad-inject"></div></ad-unit></div></a><search-results-nav-manager></search-results-nav-manager><search-results-nav data-adobe-id="global-nav-search" data-SearchResultsNavManager="search" slot="search" skeleton="chip"><search-results-controls data-SearchResultsNavManager="searchControls" slot="controls"><input aria-label="Search" data-AdsGlobalNavTakeoverManager="searchInput" data-SearchResultsNavManager="inputText:click,input,keydown" data-qa="search-input" placeholder="Search" slot="search-input" type="text" /><rt-button class="search-clear" data-qa="search-clear" data-AdsGlobalNavTakeoverManager="searchClearBtn" data-SearchResultsNavManager="clearBtn:click" size="0.875" slot="search-clear" theme="transparent"><rt-icon icon="close"></rt-icon></rt-button><rt-link class="search-submit" aria-label="Submit search" data-qa="search-submit" data-AdsGlobalNavTakeoverManager="searchSubmitBtn" data-SearchResultsNavManager="submitBtn:click" href="/search" size="0.875" slot="search-submit"><rt-icon icon="search"></rt-icon></rt-link><rt-button class="search-cancel" data-qa="search-cancel" data-AdsGlobalNavTakeoverManager="searchCancelBtn" data-SearchResultsNavManager="cancelBtn:click" size="0.875" slot="search-cancel" theme="transparent">Cancel </rt-button></search-results-controls><search-results aria-expanded="false" class="hide" data-SearchResultsNavManager="searchResults" slot="results"></search-results></search-results-nav><ul slot="nav-links"><li><a href="/about" data-qa="header:link-whats-tmeter" data-AdsGlobalNavTakeoverManager="text">About Rotten Tomatoes&reg; </a></li><li><a href="/critics" data-qa="header:link-critics-home" data-AdsGlobalNavTakeoverManager="text">Critics </a></li><li data-RtHeaderManager="loginLink"><ul><li><button id="masthead-show-login-btn" class="js-cognito-signin button--link" data-AuthInitiateManager="btnSignIn:click" data-qa="header:login-btn" data-AdsGlobalNavTakeoverManager="text">Login/signup </button></li></ul></li><li class="hide" data-RtHeaderManager="userItem:keydown,keyup,mouseenter" data-qa="header:user"><a class="masthead-user-link" data-RtHeaderManager="navUserlink:focus" rel="nofollow" data-qa="user-profile-link"><img data-RtHeaderManager="navUserImg" data-qa="user-profile-thumb"><p data-AdsGlobalNavTakeoverManager="text" data-RtHeaderManager="navUserFirstName" data-qa="user-profile-name"></p><rt-icon data-AdsGlobalNavTakeoverManager="text" icon="down-dir" image></rt-icon></a><rt-header-user-info class="hide" data-RtHeaderManager="userInfo:focusout,mouseleave"><a data-qa="user-stats-profile-pic" href="" rel="nofollow" slot="imageExpanded" tabindex="-1"><img src="" width="40" alt=""></a><a slot="fullName" rel="nofollow" href="" class="username" data-qa="user-stats-name"></a><a slot="wts" rel="nofollow" href="" class="wts-count-block" data-qa="user-stats-wts"><rt-icon icon="plus" data-qa="user-stats-ratings-count"></rt-icon><span class="count" data-qa="user-stats-wts-count"></span>&nbsp;Wants to See </a><a slot="rating" rel="nofollow" href="" class="rating-count-block" data-qa="user-stats-ratings"><rt-icon icon="star" data-qa="user-stats-ratings-count"></rt-icon><span class="count"></span>&nbsp;Ratings </a><a slot="profileLink" rel="nofollow" class="dropdown-link" href="" data-qa="user-stats-profile-link">Profile</a><a slot="accountLink" rel="nofollow" class="dropdown-link" href="/user/account" data-qa="user-stats-account-link">Account</a><a slot="logoutLink" class="dropdown-link" data-RtHeaderManager="logoutLink:click" href="#logout" data-qa="user-stats-logout-link">Log Out</a></rt-header-user-info></li></ul><rt-header-nav slot="nav-dropdowns"><button aria-label="Open aRTi" class="arti-desktop" data-ArtiManager="btnArti:click" slot="arti-desktop"><img alt="arti" src="/assets/pizza-pie/images/arti.041d204c4a4.svg" /></button><rt-header-nav-item slot="movies" data-qa="masthead:movies-dvds"><a class="unset" slot="link" href="/browse/movies_in_theaters/sort:popular" data-qa="masthead:movies-dvds-link" data-AdsGlobalNavTakeoverManager="text">Movies </a><rt-header-nav-item-dropdown aria-expanded="false" slot="dropdown" data-qa="movies-menu"><rt-header-nav-item-dropdown-list slot="column" data-qa="header-movies-in-theaters"><p slot="title" class="h4" data-qa="movies-in-theaters-main-link"><a class="unset" href="/browse/movies_in_theaters/sort:popular">Movies in theaters</a></p><ul slot="links"><li data-qa="in-theaters-item"><a href="/browse/movies_in_theaters/sort:newest" data-qa="opening-this-week-link">Opening This Week</a></li><li data-qa="in-theaters-item"><a href="/browse/movies_in_theaters/sort:top_box_office" data-qa="top-box-office-link">Top Box Office</a></li><li data-qa="in-theaters-item"><a href="/browse/movies_coming_soon/" data-qa="coming-soon-link">Coming Soon to Theaters</a></li><li data-qa="in-theaters-item"><a href="/browse/movies_in_theaters/critics:certified_fresh~sort:popular" data-qa="certified-fresh-link">Certified Fresh Movies</a></li></ul></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-on-dvd-streaming"><p slot="title" class="h4" data-qa="dvd-streaming-main-link"><a class="unset" href="/browse/movies_at_home">Movies at Home</a></p><ul slot="links"><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/affiliates:fandango-at-home" data-qa="fandango-at-home-link">Fandango at Home</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/affiliates:peacock" data-qa="peacock-link">Peacock</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/affiliates:netflix" data-qa="netflix-link">Netflix</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/affiliates:apple-tv-plus" data-qa="apple-tv-link">Apple TV+</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/affiliates:prime-video" data-qa="prime-video-link">Prime Video</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/sort:popular" data-qa="most-popular-streaming-movies-link">Most Popular Streaming movies</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/critics:certified_fresh" data-qa="certified-fresh-movies-link">Certified Fresh movies</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home" data-qa="browse-all-link">Browse all</a></li></ul></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-movies-more"><p slot="title" class="h4">More</p><ul slot="links"><li data-qa="what-to-watch-item"><a href="https://editorial.rottentomatoes.com/rt-hub/what-to-watch" class="what-to-watch" data-qa="what-to-watch-link">What to Watch<rt-badge>New</rt-badge></a></li></ul></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" cfp><p slot="title" class="h4">Certified fresh picks</p><ul slot="links" class="cfp-wrap" data-qa="header-certified-fresh-picks" data-curation="rt-nav-list-cf-picks"><li data-qa="cert-fresh-item"><a class="cfp-tile" href="/m/twinless" data-qa="cert-fresh-link"><tile-dynamic data-qa="tile"><rt-img alt="Twinless poster image" slot="image" src="https://resizing.flixster.com/j7lw2KeY9_XyfZQdqRZGku7_9C8=/206x305/v2/https://resizing.flixster.com/uxoeWz7uWmeYIV94_SzEV_osqe4=/fit-in/180x240/v2/https://resizing.flixster.com/VlylB3xT2RIYmRivMx37O3yD76Q=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2ZlNDQ1MGQ5LTFjN2QtNDIwNC04NWE1LTM5NGM4N2U5ZTgzYy5qcGc=" loading="lazy"></rt-img><div slot="caption" data-track="scores"><div class="score-wrap"><score-icon-critics certified sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" size="1" context="label">98%</rt-text></div><span class="p--small">Twinless</span><span class="sr-only">Link to Twinless</span></div></tile-dynamic></a></li><li data-qa="cert-fresh-item"><a class="cfp-tile" href="/m/hamilton_2020" data-qa="cert-fresh-link"><tile-dynamic data-qa="tile"><rt-img alt="Hamilton poster image" slot="image" src="https://resizing.flixster.com/1woquJmQfEhWCZtm7GcH0NMHsYA=/206x305/v2/https://resizing.flixster.com/PeAJ5ZpF5qB98ZiX6ixNDCgW2P0=/fit-in/180x240/v2/https://resizing.flixster.com/VmBvlTk8-z7pQvDZXTgSdj93WDE=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzkzY2IxZjFkLTE1NjEtNDQ4Yi05NDY3LTcxNzFmMDVhMDczNi5qcGc=" loading="lazy"></rt-img><div slot="caption" data-track="scores"><div class="score-wrap"><score-icon-critics certified sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" size="1" context="label">98%</rt-text></div><span class="p--small">Hamilton</span><span class="sr-only">Link to Hamilton</span></div></tile-dynamic></a></li><li data-qa="cert-fresh-item"><a class="cfp-tile" href="/m/the_thursday_murder_club" data-qa="cert-fresh-link"><tile-dynamic data-qa="tile"><rt-img alt="The Thursday Murder Club poster image" slot="image" src="https://resizing.flixster.com/jeeldFGcfSMgG09ey5VB7TCFiek=/206x305/v2/https://resizing.flixster.com/9LXDkCzIBBNEiPURkB9t6VefF5Q=/fit-in/180x240/v2/https://resizing.flixster.com/rwdeR5xIiN0k7SWr6yXdnmb6zP8=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2EzYWFkZWJiLWE5N2MtNDc3MS1iMDRlLTk0YWVlYzI5M2UxZS5qcGc=" loading="lazy"></rt-img><div slot="caption" data-track="scores"><div class="score-wrap"><score-icon-critics certified sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" size="1" context="label">76%</rt-text></div><span class="p--small">The Thursday Murder Club</span><span class="sr-only">Link to The Thursday Murder Club</span></div></tile-dynamic></a></li></ul></rt-header-nav-item-dropdown-list></rt-header-nav-item-dropdown></rt-header-nav-item><rt-header-nav-item slot="tv" data-qa="masthead:tv"><a class="unset" slot="link" href="/browse/tv_series_browse/sort:popular" data-qa="masthead:tv-link" data-AdsGlobalNavTakeoverManager="text">Tv shows </a><rt-header-nav-item-dropdown aria-expanded="false" slot="dropdown" data-qa="tv-menu"><rt-header-nav-item-dropdown-list slot="column" data-qa="header-tv-list1"><p slot="title" class="h4" data-curation="rt-hp-text-list-3">New TV Tonight </p><ul slot="links" class="score-list-wrap"><li data-qa="list-item"><a class="score-list-item" href="/tv/task/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">89%</rt-text></div><span>Task: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/the_walking_dead_daryl_dixon/s03" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">80%</rt-text></div><span>The Walking Dead: Daryl Dixon: Season 3 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/the_crow_girl/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">80%</rt-text></div><span>The Crow Girl: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/only_murders_in_the_building/s05" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>Only Murders in the Building: Season 5 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/the_girlfriend/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>The Girlfriend: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/aka_charlie_sheen/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>aka Charlie Sheen: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/wizards_beyond_waverly_place/s02" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>Wizards Beyond Waverly Place: Season 2 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/seen_and_heard_the_history_of_black_television/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>Seen &amp; Heard: the History of Black Television: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/the_fragrant_flower_blooms_with_dignity/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>The Fragrant Flower Blooms With Dignity: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/guts_and_glory/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>Guts &amp; Glory: Season 1 </span></a></li></ul><a class="a--short" data-qa="tv-list1-view-all-link" href="/browse/tv_series_browse/sort:newest" slot="view-all-link">View All </a></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-tv-list2"><p slot="title" class="h4" data-curation="rt-hp-text-list-2">Most Popular TV on RT </p><ul slot="links" class="score-list-wrap"><li data-qa="list-item"><a class="score-list-item" href="/tv/the_paper_2025/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">83%</rt-text></div><span>The Paper: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/dexter_resurrection/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">95%</rt-text></div><span>Dexter: Resurrection: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/alien_earth/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">95%</rt-text></div><span>Alien: Earth: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/task/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">89%</rt-text></div><span>Task: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/wednesday/s02" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">87%</rt-text></div><span>Wednesday: Season 2 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/peacemaker_2022/s02" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">99%</rt-text></div><span>Peacemaker: Season 2 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/the_terminal_list_dark_wolf/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">73%</rt-text></div><span>The Terminal List: Dark Wolf: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/hostage_2025/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">82%</rt-text></div><span>Hostage: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/chief_of_war/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">93%</rt-text></div><span>Chief of War: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/irish_blood/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">100%</rt-text></div><span>Irish Blood: Season 1 </span></a></li></ul><a class="a--short" data-qa="tv-list2-view-all-link" href="/browse/tv_series_browse/sort:popular?" slot="view-all-link">View All </a></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-tv-more"><p slot="title" class="h4">More</p><ul slot="links"><li><a href="https://editorial.rottentomatoes.com/rt-hub/what-to-watch/" class="what-to-watch" data-qa="what-to-watch-link-tv">What to Watch<rt-badge>New</rt-badge></a></li><li><a href="/browse/tv_series_browse/sort:popular" data-qa="tv-best-link"><span>Best TV Shows</span></a></li><li><a href="/browse/tv_series_browse/sort:popular" data-qa="tv-popular-link"><span>Most Popular TV</span></a></li><li><a href="/browse/tv_series_browse/affiliates:fandango-at-home" data-qa="tv-fandango-at-home-link"><span>Fandango at Home</span></a></li><li><a href="/browse/tv_series_browse/affiliates:peacock" data-qa="tv-peacock-link"><span>Peacock</span></a></li><li><a href="/browse/tv_series_browse/affiliates:paramount-plus" data-qa="tv-paramount-link"><span>Paramount+</span></a></li><li><a href="/browse/tv_series_browse/affiliates:netflix" data-qa="tv-netflix-link"><span>Netflix</span></a></li><li><a href="/browse/tv_series_browse/affiliates:prime-video" data-qa="tv-prime-video-link"><span>Prime Video</span></a></li><li><a href="/browse/tv_series_browse/affiliates:apple-tv-plus" data-qa="tv-apple-tv-plus-link"><span>Apple TV+</span></a></li></ul></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" cfp data-qa="header-certified-fresh-pick"><p slot="title" class="h4">Certified fresh pick </p><ul slot="links" class="cfp-wrap" data-curation="rt-nav-list-cf-picks"><li><a class="cfp-tile" href="/tv/the_paper_2025/s01" data-qa="cert-fresh-link"><tile-dynamic data-qa="tile"><rt-img alt="The Paper: Season 1 poster image" slot="image" src="https://resizing.flixster.com/yFijQcjPYUWUelgmiZLHgkXU7hw=/206x305/v2/https://resizing.flixster.com/DFkkHf5pEVX_apKtIQZcoEvI6RU=/fit-in/180x240/v2/https://resizing.flixster.com/texEZJLAG-KcVpfCdkT2R1t4cmE=/ems.cHJkLWVtcy1hc3NldHMvdHZzZWFzb24vYTM3OWM2MTctN2M3Ny00MjdhLTk4NDUtODE5ZWUwMWExNGRhLnBuZw==" loading="lazy"></rt-img><div slot="caption" data-track="scores"><div class="score-wrap"><score-icon-critics certified sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" size="1" context="label">83%</rt-text></div><span class="p--small">The Paper: Season 1</span><span class="sr-only">Link to The Paper: Season 1</span></div></tile-dynamic></a></li></ul></rt-header-nav-item-dropdown-list></rt-header-nav-item-dropdown></rt-header-nav-item><rt-header-nav-item slot="shop"><a class="unset" id="appLink" slot="link" href="https://editorial.rottentomatoes.com/article/app/" target="_blank" data-qa="masthead:app-link" data-AdsGlobalNavTakeoverManager="text">RT App <temporary-display slot="temporary-display" key="app" element="#appLink" event="click"><rt-badge hidden>New</rt-badge></temporary-display></a></rt-header-nav-item><rt-header-nav-item slot="news" data-qa="masthead:news"><a class="unset" slot="link" href="https://editorial.rottentomatoes.com/" data-qa="masthead:news-link" data-AdsGlobalNavTakeoverManager="text">News </a><rt-header-nav-item-dropdown aria-expanded="false" slot="dropdown" data-qa="news-menu"><rt-header-nav-item-dropdown-list slot="column" data-qa="header-news-columns"><p slot="title" class="h4">Columns</p><ul slot="links"><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/all-time-lists/" data-pageheader="All-Time Lists" data-qa="column-link">All-Time Lists </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/binge-guide/" data-pageheader="Binge Guide" data-qa="column-link">Binge Guide </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/comics-on-tv/" data-pageheader="Comics on TV" data-qa="column-link">Comics on TV </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/countdown/" data-pageheader="Countdown" data-qa="column-link">Countdown </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/five-favorite-films/" data-pageheader="Five Favorite Films" data-qa="column-link">Five Favorite Films </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/video-interviews/" data-pageheader="Video Interviews" data-qa="column-link">Video Interviews </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/weekend-box-office/" data-pageheader="Weekend Box Office" data-qa="column-link">Weekend Box Office </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/weekly-ketchup/" data-pageheader="Weekly Ketchup" data-qa="column-link">Weekly Ketchup </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/what-to-watch/" data-pageheader="What to Watch" data-qa="column-link">What to Watch </a></li></ul></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-news-guides"><p slot="title" class="h4">Guides</p><ul slot="links" class="news-wrap"><li data-qa="guides-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/guide/best-football-movies/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="59 Best Football Movies, Ranked by Tomatometer poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2023/09/600EssentialFootballMovies.png" loading="lazy"></rt-img><div slot="caption"><p>59 Best Football Movies, Ranked by Tomatometer</p><span class="sr-only">Link to 59 Best Football Movies, Ranked by Tomatometer</span></div></tile-dynamic></a></li><li data-qa="guides-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/guide/best-new-rom-coms-romance-movies/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="50 Best New Rom-Coms and Romance Movies poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2025/08/Best_New_Romcoms600.jpg" loading="lazy"></rt-img><div slot="caption"><p>50 Best New Rom-Coms and Romance Movies</p><span class="sr-only">Link to 50 Best New Rom-Coms and Romance Movies</span></div></tile-dynamic></a></li></ul><a class="a--short" data-qa="guides-view-all-link" href="https://editorial.rottentomatoes.com/countdown/" slot="view-all-link">View All </a></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-news-hubs"><p slot="title" class="h4">Hubs</p><ul slot="links" class="news-wrap"><li data-qa="hubs-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/rt-hub/what-to-watch/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="What to Watch: In Theaters and On Streaming poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2023/05/RT_WTW_Generic_2023_Thumbnail_600x314_021623.jpg" loading="lazy"></rt-img><div slot="caption"><p>What to Watch: In Theaters and On Streaming</p><span class="sr-only">Link to What to Watch: In Theaters and On Streaming</span></div></tile-dynamic></a></li><li data-qa="hubs-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/rt-hub/awards-tour/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="Awards Tour poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2023/02/RT_AwardsTour_Thumbnail_600x314.jpg" loading="lazy"></rt-img><div slot="caption"><p>Awards Tour</p><span class="sr-only">Link to Awards Tour</span></div></tile-dynamic></a></li></ul><a class="a--short" data-qa="hubs-view-all-link" href="https://editorial.rottentomatoes.com/rt-hubs/" slot="view-all-link">View All </a></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-news-rt-news"><p slot="title" class="h4">RT News</p><ul slot="links" class="news-wrap"><li data-qa="rt-news-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/article/new-movies-and-shows-streaming-in-september-2025-what-to-watch-on-netflix-prime-video-hbo-max-disney-and-more/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="New Movies and Shows Streaming in September: What to watch on Netflix, Prime Video, HBO Max, Disney+ and More poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2025/08/New_Streaming_September_2025-Rep.jpg" loading="lazy"></rt-img><div slot="caption"><p>New Movies and Shows Streaming in September: What to watch on Netflix, Prime Video, HBO Max, Disney+ and More</p><span class="sr-only">Link to New Movies and Shows Streaming in September: What to watch on Netflix, Prime Video, HBO Max, Disney+ and More</span></div></tile-dynamic></a></li><li data-qa="rt-news-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/article/the-conjuring-last-rites-first-reviews/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="<em>The Conjuring: Last Rites</em>First Reviews: A Frightful, Fitting Send-off poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2025/09/Conjuring_Last_Rites_Reviews-Rep.jpg" loading="lazy"></rt-img><div slot="caption"><p><em>The Conjuring: Last Rites</em>First Reviews: A Frightful, Fitting Send-off</p><span class="sr-only">Link to <em>The Conjuring: Last Rites</em>First Reviews: A Frightful, Fitting Send-off</span></div></tile-dynamic></a></li></ul><a class="a--short" data-qa="rt-news-view-all-link" href="https://editorial.rottentomatoes.com/news/" slot="view-all-link">View All </a></rt-header-nav-item-dropdown-list></rt-header-nav-item-dropdown></rt-header-nav-item><rt-header-nav-item slot="showtimes"><a class="unset" slot="link" href="https://www.fandango.com/movies-in-theaters?a=13036" target="_blank" rel="noopener" data-qa="masthead:tickets-showtimes-link" data-AdsGlobalNavTakeoverManager="text">Showtimes </a></rt-header-nav-item></rt-header-nav></rt-header><ads-global-nav-takeover-manager></ads-global-nav-takeover-manager><section class="trending-bar"><ad-unit hidden id="trending_bar_ad" unit-display="desktop" unit-type="trendinggraphic"><div slot="ad-inject"></div></ad-unit><div id="trending-bar-start" class="trending-list-wrap" data-qa="trending-bar"><ul class="list-inline trending-bar__list" data-curation="rt-nav-trending" data-qa="trending-bar-list"><li class="trending-bar__header">Trending on RT</li><li><a class="trending-bar__link" href="https://editorial.rottentomatoes.com/article/2025-emmys-ballot-complete-with-tomatometer-and-popcornmeter-scores/" data-qa="trending-bar-item">Emmy Noms </a></li><li><a class="trending-bar__link" href="https://editorial.rottentomatoes.com/article/movie-re-releases-calendar/" data-qa="trending-bar-item">Re-Release Calendar </a></li><li><a class="trending-bar__link" href="https://editorial.rottentomatoes.com/article/renewed-and-cancelled-tv-shows-2025/" data-qa="trending-bar-item">Renewed and Cancelled TV </a></li><li><a class="trending-bar__link" href="https://editorial.rottentomatoes.com/article/app/" data-qa="trending-bar-item">The Rotten Tomatoes App </a></li></ul><div class="trending-bar__social" data-qa="trending-bar-social-list"><social-media-icons theme="light" size="14"></social-media-icons></div></div></section><main id="main_container" class="container rt-layout__content"><div id="main-page-content"><div id="tv-series-overview" data-HeroModulesManager="overviewWrap"><watchlist-button-manager></watchlist-button-manager><div id="hero-wrap" data-AdUnitManager="heroWrap" data-AdsMediaScorecardManager="heroWrap" data-HeroModulesManager="heroWrap"><div aria-labelledby="media-hero-label" class="media-hero-wrap" skeleton="panel" data-adobe-id="media-hero" data-qa="section:media-hero" data-HeroModulesManager="mediaHeroWrap"><h1 class="unset" id="media-hero-label"><sr-text>Peacemaker </sr-text></h1><media-hero averagecolor="33,54,15" mediatype="TvSeries" scrolly="0" scrollystart="0" data-AdsMediaScorecardManager="mediaHero" data-HeroModulesManager="mediaHero:collapse"><rt-button slot="iconicVideoCta" theme="transparent" data-content-type="PROMO" data-ems-id="c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed" data-mpx-id="2447231043621" data-public-id="nTePljVEct61" data-title="Peacemaker" data-type="TvSeries" data-VideoPlayerOverlayManager="btnVideo:click"><sr-text>Play trailer</sr-text></rt-button><rt-text slot="iconicVideoRuntime" size="0.75">1:39</rt-text><rt-img slot="iconic" alt="Main image for Peacemaker" fallbacktheme="iconic" fetchpriority="high" src="https://resizing.flixster.com/bvZfzyIQHc8UI0CJS9UdOdRXC7w=/375x210/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/166/403/thumb_47CFBBB8-95A3-4634-8B02-581F2C757D98.jpg,https://resizing.flixster.com/2jGm07y7TAmgwksV1KSg9Xsogtg=/620x336/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/166/403/thumb_47CFBBB8-95A3-4634-8B02-581F2C757D98.jpg"></rt-img><img slot="poster" alt="Poster for " fetchpriority="high" src="https://resizing.flixster.com/fxG7Llnq_i5HAiGsTWJ3fB5a29Q=/68x102/v2/https://resizing.flixster.com/mEY3rZ4CClCMjpOT2WJSWBvf_p8=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvOTY5OTU0MzYtZmM5MC00MmI4LWI1YTEtYjQ4NDU2MWRmZWMzLmpwZw==" /><rt-text slot="title" size="1.25,1.75" context="heading">Peacemaker</rt-text><rt-text slot="episodeTitle" size="1,1.5" context="label"></rt-text><rt-text slot="metadataProp" context="label" size="0.875">TV-MA</rt-text><rt-text slot="metadataProp" context="label" size="0.875">Next Ep Thu Sep 11</rt-text><rt-text slot="metadataProp" context="label" size="0.875">2 Seasons</rt-text><rt-text slot="metadataGenre" size="0.875">Comedy</rt-text><rt-text slot="metadataGenre" size="0.875">Action</rt-text><rt-button slot="trailerCta" shape="pill" theme="light" data-content-type="PROMO" data-ems-id="c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed" data-mpx-id="2447231043621" data-public-id="nTePljVEct61" data-title="Peacemaker" data-type="TvSeries" data-VideoPlayerOverlayManager="btnVideo:click"><rt-icon icon="play"></rt-icon><sr-text>Play</sr-text>Trailer </rt-button><watchlist-button slot="watchlistCta" emsid="c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed" mediatype="TvSeries" mediatitle="Peacemaker" state="unchecked" theme="transparent-lighttext" data-HeroModulesManager="mediaHeroWatchlistBtn" data-WatchlistButtonManager="watchlistButton:click"><span slot="text">Watchlist</span></watchlist-button><watchlist-button slot="mobileWatchlistCta" emsid="c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed" mediatype="TvSeries" mediatitle="Peacemaker" state="unchecked" data-HeroModulesManager="mediaHeroWatchlistBtn" data-WatchlistButtonManager="watchlistButton:click"></watchlist-button><div slot="desktopVideos" data-HeroModulesManager="mediaHeroVideos"></div><rt-button slot="collapsedPrimaryCta" hidden shape="pill" theme="simplified" data-AdsMediaScorecardManager="collapsedPrimaryCta" data-HeroModulesManager="mediaHeroCta:click"></rt-button><watchlist-button slot="collapsedWatchlistCta" emsid="c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed" mediatype="TvSeries" mediatitle="Peacemaker" state="unchecked" theme="transparent-lighttext" data-HeroModulesManager="mediaHeroWatchlistBtn" data-WatchlistButtonManager="watchlistButton:click"><span slot="text">Watchlist</span></watchlist-button><score-icon-critics slot="collapsedCriticsIcon" size="2.5"></score-icon-critics><rt-text slot="collapsedCriticsScore" context="label" size="1.375"></rt-text><rt-link slot="collapsedCriticsLink" size="0.75"></rt-link><rt-text slot="collapsedCriticsLabel" size="0.75">Tomatometer</rt-text><score-icon-audience slot="collapsedAudienceIcon" size="2.5"></score-icon-audience><rt-text slot="collapsedAudienceScore" context="label" size="1.375"></rt-text><rt-link slot="collapsedAudienceLink" size="0.75"></rt-link><rt-text slot="collapsedAudienceLabel" size="0.75">Popcornmeter</rt-text></media-hero><script id="media-hero-json" data-json="mediaHero" type="application/json">{"averageColorHsl":"33,54,15","iconic":{"srcDesktop":"https://resizing.flixster.com/2jGm07y7TAmgwksV1KSg9Xsogtg=/620x336/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/166/403/thumb_47CFBBB8-95A3-4634-8B02-581F2C757D98.jpg","srcMobile":"https://resizing.flixster.com/bvZfzyIQHc8UI0CJS9UdOdRXC7w=/375x210/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/166/403/thumb_47CFBBB8-95A3-4634-8B02-581F2C757D98.jpg"},"content":{"episodeTitle":"","metadataGenres":["Comedy","Action"],"metadataProps":["TV-MA","Next Ep Thu Sep 11","2 Seasons"],"posterSrc":"https://resizing.flixster.com/fxG7Llnq_i5HAiGsTWJ3fB5a29Q=/68x102/v2/https://resizing.flixster.com/mEY3rZ4CClCMjpOT2WJSWBvf_p8=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvOTY5OTU0MzYtZmM5MC00MmI4LWI1YTEtYjQ4NDU2MWRmZWMzLmpwZw==","title":"Peacemaker","primaryVideo":{"contentType":"PROMO","durationInSeconds":"99.933","mpxId":"2447231043621","publicId":"nTePljVEct61","thumbnail":{"url":"https://statcdn.fandango.com/MPX/image/NBCU_Fandango/166/403/thumb_47CFBBB8-95A3-4634-8B02-581F2C757D98.jpg"},"title":"Peacemaker: Season 2 Trailer - Weeks Ahead","runtime":"1:39"}}} </script></div><hero-modules-manager><script data-json="vanity" type="application/json">{"emsId":"c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed","href":"/tv/peacemaker_2022","lifecycleWindow":{"date":"2025-09-11","lifecycle":"AIRING"},"type":"tvSeries","title":"Peacemaker","value":"peacemaker_2022","parents":[],"mediaType":"TvSeries"}</script></hero-modules-manager></div><div id="main-wrap"><div id="modules-wrap" data-curation="drawer"><div class="media-scorecard no-border" data-adobe-id="media-scorecard" data-qa="section:media-scorecard"><media-scorecard hideaudiencescore="false" skeleton="panel" data-AdsMediaScorecardManager="mediaScorecard" data-HeroModulesManager="mediaScorecard"><rt-img alt="poster image" loading="lazy" slot="posterImage" src="https://resizing.flixster.com/UHglta_RX5_h8fsHS2BljZkvfZk=/206x305/v2/https://resizing.flixster.com/mEY3rZ4CClCMjpOT2WJSWBvf_p8=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvOTY5OTU0MzYtZmM5MC00MmI4LWI1YTEtYjQ4NDU2MWRmZWMzLmpwZw=="></rt-img><rt-button slot="criticsScoreIcon" data-MediaScorecardManager="overlayOpen:click" theme="transparent"><score-icon-critics certified="false" sentiment="POSITIVE" size="2.5"></score-icon-critics></rt-button><rt-text slot="criticsScore" context="label" role="button" size="1.375" data-MediaScorecardManager="overlayOpen:click">96%</rt-text><rt-text slot="criticsScoreType" class="critics-score-type" role="button" size="0.75" data-MediaScorecardManager="overlayOpen:click">Avg. Tomatometer</rt-text><rt-link slot="criticsReviews" size="0.75" href="">150 Reviews </rt-link><rt-button slot="audienceScoreIcon" data-MediaScorecardManager="overlayOpen:click" theme="transparent"><score-icon-audience certified="false" size="2.5" sentiment="POSITIVE"></score-icon-audience></rt-button><rt-text slot="audienceScore" context="label" role="button" size="1.375" data-MediaScorecardManager="overlayOpen:click">84%</rt-text><rt-text slot="audienceScoreType" class="audience-score-type" role="button" size="0.75" data-MediaScorecardManager="overlayOpen:click">Avg. Popcornmeter</rt-text><rt-link slot="audienceReviews" size="0.75" href="">2,500+ Ratings </rt-link><div slot="description" data-AdsMediaScorecardManager="description"><drawer-more maxlines="2" skeleton="panel" status="closed" style="--display: flex; gap: 4px;"><rt-text slot="content" size="1">A man fights for peace at any cost, no matter how many people he has to kill to get it. </rt-text><rt-link slot="ctaOpen"><rt-icon icon="down-open"></rt-icon></rt-link><rt-link slot="ctaClose"><rt-icon icon="up-open"></rt-icon></rt-link></drawer-more></div><affiliate-icon data-AdsMediaScorecardManager="affiliateIcon" icon="fandango-at-home" slot="affiliateIcon"></affiliate-icon><rt-img data-AdsMediaScorecardManager="affiliateIconCustom" slot="affiliateIconCustom" hidden></rt-img><rt-text context="label" data-AdsMediaScorecardManager="affiliatePrimaryText" size="1" slot="affiliatePrimaryText">Watch on Fandango at Home</rt-text><rt-text data-AdsMediaScorecardManager="affiliateSecondaryText" size="0.75" slot="affiliateSecondaryText"></rt-text><rt-button arialabel="Stream Peacemaker on Fandango at Home" href="https://athome.fandango.com/content/browse/details/Peacemaker-A-Whole-New-Whirled/2089756?cmp=rt_leaderboard" rel="noopener" shape="pill" slot="affiliateCtaBtn" style="--backgroundColor: #3478C1; --textColor: #FFFFFF;" target="_blank" theme="simplified" data-AdsMediaScorecardManager="affiliateCtaBtn" data-HeroModulesManager="mediaScorecardCta:click">Stream Now </rt-button><div slot="adImpressions"></div></media-scorecard><media-scorecard-manager><script id="media-scorecard-json" data-json="mediaScorecard" type="application/json">{"audienceScore":{"bandedRatingCount":"2,500+ Ratings","score":"84","scoreType":"ALL","sentiment":"POSITIVE","certified":false,"scorePercent":"84%","title":"Avg. Popcornmeter"},"criticsScore":{"averageRating":"7.80","certified":false,"likedCount":143,"notLikedCount":7,"ratingCount":150,"reviewCount":150,"score":"96","sentiment":"POSITIVE","scorePercent":"96%","title":"Avg. Tomatometer"},"cta":{"affiliate":"fandango-at-home","buttonStyle":{"backgroundColor":"#3478C1","textColor":"#FFFFFF"},"buttonText":"Stream Now","buttonAnnouncement":"Stream Peacemaker on Fandango at Home","buttonUrl":"https://athome.fandango.com/content/browse/details/Peacemaker-A-Whole-New-Whirled/2089756?cmp=rt_leaderboard","icon":"fandango-at-home","windowDate":"","windowText":"Watch on Fandango at Home"},"description":"A man fights for peace at any cost, no matter how many people he has to kill to get it.","hideAudienceScore":false,"overlay":{"audienceAll":{"bandedRatingCount":"2,500+ Ratings","score":"84","scoreType":"ALL","sentiment":"POSITIVE","certified":false,"scorePercent":"84%","title":"Avg. Popcornmeter"},"audienceTitle":"Avg. Popcornmeter","audienceVerified":{"title":"Avg. Popcornmeter"},"criticsAll":{"averageRating":"7.80","certified":false,"likedCount":143,"notLikedCount":7,"ratingCount":150,"reviewCount":150,"score":"96","sentiment":"POSITIVE","scorePercent":"96%","title":"Avg. Tomatometer","scoreLinkText":"150 Reviews"},"criticsTitle":"Avg. Tomatometer","criticsTop":{"averageRating":"7.40","certified":false,"likedCount":36,"notLikedCount":2,"ratingCount":38,"reviewCount":38,"score":"95","sentiment":"POSITIVE","scorePercent":"95%","title":"Avg. Tomatometer","scoreLinkText":"38 Top Critic Reviews"},"hasAudienceAll":true,"hasAudienceVerified":false,"hasCriticsAll":true,"hasCriticsTop":true,"mediaType":"TvSeries","showScoreDetailsAudience":true,"learnMoreUrl":"https://editorial.rottentomatoes.com/article/introducing-verified-audience-score/"},"primaryImageUrl":"https://resizing.flixster.com/UHglta_RX5_h8fsHS2BljZkvfZk=/206x305/v2/https://resizing.flixster.com/mEY3rZ4CClCMjpOT2WJSWBvf_p8=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvOTY5OTU0MzYtZmM5MC00MmI4LWI1YTEtYjQ4NDU2MWRmZWMzLmpwZw=="} </script></media-scorecard-manager></div><section class="modules-nav" data-ModulesNavigationManager="navWrap"><modules-navigation-manager></modules-navigation-manager><nav><modules-navigation-carousel skeleton="panel" tilewidth="auto" data-ModulesNavigationManager="navCarousel"><a slot="tile" href="#where-to-watch"><rt-tab data-ModulesNavigationManager="navTab">Where to Watch</rt-tab></a><a slot="tile" href="#seasons"><rt-tab data-ModulesNavigationManager="navTab">Seasons</rt-tab></a><a slot="tile" href="#cast-and-crew"><rt-tab data-ModulesNavigationManager="navTab">Cast &amp; Crew</rt-tab></a><a slot="tile" href="#more-like-this"><rt-tab data-ModulesNavigationManager="navTab">More Like This</rt-tab></a><a slot="tile" href="#news-and-guides"><rt-tab data-ModulesNavigationManager="navTab">Related News</rt-tab></a><a slot="tile" href="#videos"><rt-tab data-ModulesNavigationManager="navTab">Videos</rt-tab></a><a slot="tile" href="#photos"><rt-tab data-ModulesNavigationManager="navTab">Photos</rt-tab></a><a slot="tile" href="#media-info"><rt-tab data-ModulesNavigationManager="navTab">Media Info</rt-tab></a></modules-navigation-carousel></nav></section><div class="modules-layout" tabindex="-1" data-ModulesNavigationManager="content:focusin"><div id="where-to-watch" class="dom-anchor" data-ModulesNavigationManager="domAnchor"></div><section aria-labelledby="where-to-watch-label" class="where-to-watch" data-adobe-id="where-to-watch" data-qa="section:where-to-watch"><div class="header-wrap"><h2 class="unset" id="where-to-watch-label"><rt-text context="heading" size="1.25" style="--textTransform: capitalize;">Where to Watch</rt-text></h2><h3 class="unset"><rt-text context="heading" size="0.75" style="--textColor: var(--grayDark4); --letterSpacing: 1px; --textTransform: capitalize;">Peacemaker </rt-text></h3></div><where-to-watch-manager><script id="where-to-watch-json" data-json="whereToWatch" type="application/json">{"affiliates":[{"icon":"fandango-at-home","url":"https://athome.fandango.com/content/browse/details/Peacemaker-A-Whole-New-Whirled/2089756?cmp=rt_where_to_watch","isSponsoredLink":false,"text":"Fandango at Home","availableSeasons":"Season 1"},{"icon":"max-us","url":"https://max.prf.hn/click/camref:1100lqRUT?cmp=rt_where_to_watch","isSponsoredLink":true,"text":"Max","availableSeasons":"Seasons 1-2"}],"affiliatesText":"Watch Peacemaker with a subscription on Max, or buy it on Fandango at Home.","justWatchMediaType":"show","showtimesUrl":"","releaseYear":"2022","tarsSlug":"rt-affiliates-sort-order","title":"Peacemaker"} </script></where-to-watch-manager><div hidden data-WhereToWatchManager="jwContainer"></div><div hidden data-WhereToWatchManager="w2wContainer"><carousel-slider data-curation="rt-affiliates-sort-order" gap="15px" skeleton="panel" tile-width="80px" exclude-page-indicators><where-to-watch-meta affiliate="fandango-at-home" data-qa="affiliate-item" href="https://athome.fandango.com/content/browse/details/Peacemaker-A-Whole-New-Whirled/2089756?cmp=rt_where_to_watch" issponsoredlink="false" skeleton="panel" slot="tile"><where-to-watch-bubble image="fandango-at-home" slot="bubble" tabindex="-1"></where-to-watch-bubble><span slot="license">Fandango at Home</span><span slot="coverage">Season 1</span></where-to-watch-meta><where-to-watch-meta affiliate="max-us" data-qa="affiliate-item" href="https://max.prf.hn/click/camref:1100lqRUT?cmp=rt_where_to_watch" issponsoredlink="true" skeleton="panel" slot="tile"><where-to-watch-bubble image="max-us" slot="bubble" tabindex="-1"></where-to-watch-bubble><span slot="license">Max</span><span slot="coverage">Seasons 1-2</span></where-to-watch-meta></carousel-slider><p class="affiliates-text">Watch Peacemaker with a subscription on Max, or buy it on Fandango at Home. </p></div></section></div><div class="modules-layout" tabindex="-1" data-ModulesNavigationManager="content:focusin"><div id="seasons" class="dom-anchor" data-ModulesNavigationManager="domAnchor"></div><section aria-labelledby="seasons-label" data-adobe-id="seasons" data-qa="section:seasons"><div class="header-wrap"><h2 class="unset" id="seasons-label"><rt-text size="1.25" context="heading">Seasons</rt-text></h2></div><div class="content-wrap"><carousel-slider tile-width="240"><tile-season slot="tile" href="/tv/peacemaker_2022/s02" skeleton="panel"><rt-img alt="Season 2" slot="image" src="https://resizing.flixster.com/icqaMuFdXKqmN8qur-dfG4I-hWs=/206x305/v2/https://resizing.flixster.com/J-WXxwHSdv6w7VHxcCrPdebOvUA=/ems.cHJkLWVtcy1hc3NldHMvdHZzZWFzb24vNmE4MmUxOWItNGY5Yy00YzY0LTk5ODktZDY1MGEzZjdmZDFhLmpwZw=="></rt-img><rt-text size="1" context="label" slot="title" style="--letterSpacing: 1px;">Season 2</rt-text><score-icon-critics certified="true" sentiment="POSITIVE" size="0.875" slot="criticsIcon"></score-icon-critics><rt-text slot="criticsScore" size="0.75" context="label">99%</rt-text><rt-text size="0.875" slot="airDate">Next Ep Thu Sep 11</rt-text><rt-text size="0.875" context="label" slot="details">Details <rt-icon icon="right-chevron"></rt-icon></rt-text></tile-season><tile-season slot="tile" href="/tv/peacemaker_2022/s01" skeleton="panel"><rt-img alt="Season 1" slot="image" src="https://resizing.flixster.com/5u5bKuemqBmiK2W2Xuxb7xBnuZI=/206x305/v2/https://resizing.flixster.com/YjquXjsEOPntiHVUvZlTljYnlZw=/ems.cHJkLWVtcy1hc3NldHMvdHZzZWFzb24vNDJkODE0MDctYjAzNy00MWIyLTlmMjgtZDM5YWY3MDI0YjUzLmpwZw=="></rt-img><rt-text size="1" context="label" slot="title" style="--letterSpacing: 1px;">Season 1</rt-text><score-icon-critics certified="true" sentiment="POSITIVE" size="0.875" slot="criticsIcon"></score-icon-critics><rt-text slot="criticsScore" size="0.75" context="label">93%</rt-text><rt-text size="0.875" slot="airDate">2022</rt-text><rt-text size="0.875" context="label" slot="details">Details <rt-icon icon="right-chevron"></rt-icon></rt-text></tile-season></carousel-slider></div></section></div><div class="modules-layout" tabindex="-1" data-ModulesNavigationManager="content:focusin"><div id="cast-and-crew" class="dom-anchor" data-ModulesNavigationManager="domAnchor"></div><section aria-labelledby="cast-and-crew-label" class="cast-and-crew" data-adobe-id="cast-and-crew" data-qa="section:cast-and-crew"><div class="header-wrap"><h2 class="unset" id="cast-and-crew-label"><rt-text size="1.25" context="heading" data-qa="title">Cast & Crew</rt-text></h2><rt-button arialabel="Cast and Crew" data-qa="view-all-link" href="/tv/peacemaker_2022/cast-and-crew" shape="pill" size="0.875" style="--buttonPadding: 6px 24px;--backgroundColor: var(--grayLight1);--borderColor: transparent;--letterSpacing: 1px;" theme="light">View All </rt-button></div><div class="content-wrap"><a href="/celebrity/john_cena" data-qa="person-item"><tile-dynamic skeleton="panel"><rt-img alt="John Cena thumbnail image" aria-hidden="true" loading="lazy" slot="image" src="https://resizing.flixster.com/qFr2ZK1qYDkqSmM5eT3nz_n6E_g=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/487578_v9_ba.jpg"></rt-img><div slot="insetText" aria-label="John Cena, Peacemaker"><p class="name" data-qa="person-name">John Cena</p><p class="role" data-qa="person-role">Peacemaker</p></div></tile-dynamic></a><a href="/celebrity/danielle_brooks" data-qa="person-item"><tile-dynamic skeleton="panel"><rt-img alt="Danielle Brooks thumbnail image" aria-hidden="true" loading="lazy" slot="image" src="https://resizing.flixster.com/KhnY5vsfjM0vtw0cZL3aNxXbeUE=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/765589_v9_bc.jpg"></rt-img><div slot="insetText" aria-label="Danielle Brooks, Leota Adebayo"><p class="name" data-qa="person-name">Danielle Brooks</p><p class="role" data-qa="person-role">Leota Adebayo</p></div></tile-dynamic></a><a href="/celebrity/freddie_stroma" data-qa="person-item"><tile-dynamic skeleton="panel"><rt-img alt="Freddie Stroma thumbnail image" aria-hidden="true" loading="lazy" slot="image" src="https://resizing.flixster.com/Yk2eiDCtamfmNlK-xMa7nmEw_Po=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/GNLZZGG00283ZZD.jpg"></rt-img><div slot="insetText" aria-label="Freddie Stroma, Vigilante"><p class="name" data-qa="person-name">Freddie Stroma</p><p class="role" data-qa="person-role">Vigilante</p></div></tile-dynamic></a><a href="/celebrity/chukwudi_iwuji" data-qa="person-item"><tile-dynamic skeleton="panel"><rt-img alt="Chukwudi Iwuji thumbnail image" aria-hidden="true" loading="lazy" slot="image" src="https://resizing.flixster.com/uNAFlG9dNMjJwyMbPDiCsbjkX8I=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/565157_v9_ba.jpg"></rt-img><div slot="insetText" aria-label="Chukwudi Iwuji, Clemson Murn"><p class="name" data-qa="person-name">Chukwudi Iwuji</p><p class="role" data-qa="person-role">Clemson Murn</p></div></tile-dynamic></a><a href="/celebrity/jennifer_holland" data-qa="person-item"><tile-dynamic skeleton="panel"><rt-img alt="Jennifer Holland thumbnail image" aria-hidden="true" loading="lazy" slot="image" src="https://resizing.flixster.com/-xeYAf0O7fGIQHRx_YkL7vnaMMg=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/331642_v9_bb.jpg"></rt-img><div slot="insetText" aria-label="Jennifer Holland, Emilia Harcourt"><p class="name" data-qa="person-name">Jennifer Holland</p><p class="role" data-qa="person-role">Emilia Harcourt</p></div></tile-dynamic></a><a href="/celebrity/steve_agee" data-qa="person-item"><tile-dynamic skeleton="panel"><rt-img alt="Steve Agee thumbnail image" aria-hidden="true" loading="lazy" slot="image" src="https://resizing.flixster.com/YprPSg0SXNIqq-Wy4UEz4ovBnOw=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/223358_v9_bd.jpg"></rt-img><div slot="insetText" aria-label="Steve Agee, John Economos"><p class="name" data-qa="person-name">Steve Agee</p><p class="role" data-qa="person-role">John Economos</p></div></tile-dynamic></a></div></section></div><ad-unit hidden unit-display="desktop" unit-type="opbannerone"><div slot="ad-inject" class="banner-ad"></div></ad-unit><ad-unit hidden unit-display="mobile" unit-type="interscroller" no-retry data-AdUnitManager="adUnit:interscrollerinstantiated"><aside slot="ad-inject" class="center mobile-interscroller"></aside></ad-unit><div class="modules-layout" tabindex="-1" data-ModulesNavigationManager="content:focusin"><div id="more-like-this" class="dom-anchor" data-ModulesNavigationManager="domAnchor"></div><section aria-labelledby="more-like-this-label" class="more-like-this" data-adobe-id="more-like-this" data-qa="section:more-like-this"><div class="header-wrap"><div class="link-wrap"><h3 class="unset" id="more-like-this-label"><rt-text size="1.25" context="heading">More Like This </rt-text></h3><rt-button arialabel="Popular TV on Streaming" data-qa="view-all-link" href="/browse/tv_series_browse/sort:popular" shape="pill" size="0.875" style="--buttonPadding: 6px 24px;--backgroundColor: var(--grayLight1);--borderColor: transparent;--letterSpacing: 1px;" theme="light">View All </rt-button></div></div><div class="content-wrap"><carousel-slider skeleton="panel" tile-width="140px" gap="15px"><tile-poster-card slot="tile"><rt-link slot="primaryImage" href="/tv/twisted_metal" tabindex="-1"><sr-text>Twisted Metal</sr-text><rt-img loading="" src="https://resizing.flixster.com/MtyzaFnLaDY2B3SeRCOm91EwADE=/206x305/v2/https://resizing.flixster.com/mAAW4s6Bzl9wVHeH6GYXImTQFYY=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvNWNlYzRjODUtYzU0OS00NzJhLTk5NmQtOTgwOTg1MTlkYWJjLmpwZw==" alt="Twisted Metal poster"></rt-img></rt-link><score-icon-critics certified="false" sentiment="POSITIVE" size="1" slot="criticsIcon" verticalalign="sub"></score-icon-critics><rt-text slot="criticsScore" size="0.9" context="label">79% </rt-text><score-icon-audience certified="false" sentiment="POSITIVE" size="1" slot="audienceIcon"></score-icon-audience><rt-text slot="audienceScore" size="0.9" context="label">88% </rt-text><rt-link slot="title" href="/tv/twisted_metal" size="0.85" context="label">Twisted Metal </rt-link><watchlist-button data-WatchlistButtonManager="watchlistButton:click" emsid="80499e3c-a069-3e48-9d23-5b72d9f58079" mediatype="TvSeries" mediatitle="Twisted Metal" slot="watchlistButton" state="unchecked"><span slot="text">Watchlist</span></watchlist-button><rt-button data-content-type="PROMO" data-disable-ads="" data-ems-id="80499e3c-a069-3e48-9d23-5b72d9f58079" data-mpx-id="2438157891760" data-position="1" data-public-id="sGoBrIyuO6Gy" data-title="Twisted Metal: Season 2 Trailer" data-track="poster" data-type="TvSeries" data-VideoPlayerOverlayManager="btnVideo:click" data-video-list="" slot="trailerButton" size="0.875" theme="transparent"><rt-icon icon="play"></rt-icon><span>TRAILER</span><sr-text>for Twisted Metal</sr-text></rt-button></tile-poster-card><tile-poster-card slot="tile"><rt-link slot="primaryImage" href="/tv/comrade_detective" tabindex="-1"><sr-text>Comrade Detective</sr-text><rt-img loading="" src="https://resizing.flixster.com/L44TL1O_i8N47QRPZ1DpAjipU78=/206x305/v2/https://resizing.flixster.com/sUXscZBjGl80M7C8wEX9qISu3Ls=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvUlRUVjI1OTA1OC53ZWJw" alt="Comrade Detective poster"></rt-img></rt-link><score-icon-critics certified="false" sentiment="POSITIVE" size="1" slot="criticsIcon" verticalalign="sub"></score-icon-critics><rt-text slot="criticsScore" size="0.9" context="label">85% </rt-text><score-icon-audience certified="false" sentiment="POSITIVE" size="1" slot="audienceIcon"></score-icon-audience><rt-text slot="audienceScore" size="0.9" context="label">88% </rt-text><rt-link slot="title" href="/tv/comrade_detective" size="0.85" context="label">Comrade Detective </rt-link><watchlist-button data-WatchlistButtonManager="watchlistButton:click" emsid="bcb4d43d-3dbc-3da9-8051-51454a471ea1" mediatype="TvSeries" mediatitle="Comrade Detective" slot="watchlistButton" state="unchecked"><span slot="text">Watchlist</span></watchlist-button></tile-poster-card><tile-poster-card slot="tile"><rt-link slot="primaryImage" href="/tv/saul_of_the_mole_men" tabindex="-1"><sr-text>Saul of the Mole Men</sr-text><rt-img loading="" src="https://resizing.flixster.com/vvIDMwPetdv8iLBHM9DCici60ag=/206x305/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p284327_b_v8_ab.jpg" alt="Saul of the Mole Men poster"></rt-img></rt-link><score-icon-critics certified="false" sentiment="NEGATIVE" size="1" slot="criticsIcon" verticalalign="sub"></score-icon-critics><rt-text slot="criticsScore" size="0.9" context="label">17% </rt-text><score-icon-audience certified="false" sentiment="" size="1" slot="audienceIcon"></score-icon-audience><rt-text slot="audienceScore" size="0.9" context="label">% </rt-text><rt-link slot="title" href="/tv/saul_of_the_mole_men" size="0.85" context="label">Saul of the Mole Men </rt-link><watchlist-button data-WatchlistButtonManager="watchlistButton:click" emsid="19f88158-8f52-3e02-b11e-89f20b21eb4e" mediatype="TvSeries" mediatitle="Saul of the Mole Men" slot="watchlistButton" state="unchecked"><span slot="text">Watchlist</span></watchlist-button></tile-poster-card><tile-poster-card slot="tile"><rt-link slot="primaryImage" href="/tv/somebody_somewhere" tabindex="-1"><sr-text>Somebody Somewhere</sr-text><rt-img loading="" src="https://resizing.flixster.com/TJvPpdNIt4ic6QlG5Kwgkf80PZo=/206x305/v2/https://resizing.flixster.com/v8tcpv_dwS6GbygTvvUXubTn9_w=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvNTNiNTUwMzUtMzQyNi00NGM3LTkzNTgtMjU0NzU2MGU4NmE4LmpwZw==" alt="Somebody Somewhere poster"></rt-img></rt-link><score-icon-critics certified="false" sentiment="POSITIVE" size="1" slot="criticsIcon" verticalalign="sub"></score-icon-critics><rt-text slot="criticsScore" size="0.9" context="label">100% </rt-text><score-icon-audience certified="false" sentiment="POSITIVE" size="1" slot="audienceIcon"></score-icon-audience><rt-text slot="audienceScore" size="0.9" context="label">93% </rt-text><rt-link slot="title" href="/tv/somebody_somewhere" size="0.85" context="label">Somebody Somewhere </rt-link><watchlist-button data-WatchlistButtonManager="watchlistButton:click" emsid="5cddaabc-0c8d-3e41-b44b-c44487f54cc9" mediatype="TvSeries" mediatitle="Somebody Somewhere" slot="watchlistButton" state="unchecked"><span slot="text">Watchlist</span></watchlist-button><rt-button data-content-type="PROMO" data-disable-ads="" data-ems-id="5cddaabc-0c8d-3e41-b44b-c44487f54cc9" data-mpx-id="2378416707669" data-position="4" data-public-id="wnDPGb8gSEC7" data-title="Somebody Somewhere: Season 3 Trailer" data-track="poster" data-type="TvSeries" data-VideoPlayerOverlayManager="btnVideo:click" data-video-list="" slot="trailerButton" size="0.875" theme="transparent"><rt-icon icon="play"></rt-icon><span>TRAILER</span><sr-text>for Somebody Somewhere</sr-text></rt-button></tile-poster-card><tile-poster-card slot="tile"><rt-link slot="primaryImage" href="/tv/south_side" tabindex="-1"><sr-text>South Side</sr-text><rt-img loading="" src="none" alt="South Side poster"></rt-img></rt-link><score-icon-critics certified="false" sentiment="POSITIVE" size="1" slot="criticsIcon" verticalalign="sub"></score-icon-critics><rt-text slot="criticsScore" size="0.9" context="label">100% </rt-text><score-icon-audience certified="false" sentiment="POSITIVE" size="1" slot="audienceIcon"></score-icon-audience><rt-text slot="audienceScore" size="0.9" context="label">92% </rt-text><rt-link slot="title" href="/tv/south_side" size="0.85" context="label">South Side </rt-link><watchlist-button data-WatchlistButtonManager="watchlistButton:click" emsid="6942346f-6675-39af-945a-1c6c6d526cef" mediatype="TvSeries" mediatitle="South Side" slot="watchlistButton" state="unchecked"><span slot="text">Watchlist</span></watchlist-button><rt-button data-content-type="PROMO" data-disable-ads="" data-ems-id="6942346f-6675-39af-945a-1c6c6d526cef" data-mpx-id="2130332739528" data-position="5" data-public-id="RFlaFZLoP7L5" data-title="South Side: Season 3 Trailer" data-track="poster" data-type="TvSeries" data-VideoPlayerOverlayManager="btnVideo:click" data-video-list="" slot="trailerButton" size="0.875" theme="transparent"><rt-icon icon="play"></rt-icon><span>TRAILER</span><sr-text>for South Side</sr-text></rt-button></tile-poster-card><tile-poster-card skeleton="panel" slot="tile" tabindex="-1"><tile-view-more aspect="posterCard" background="collage" slot="primaryImage"></tile-view-more><rt-text slot="title" size="0.85" context="label">Discover more movies and TV shows.</rt-text><rt-button href="/browse/tv_series_browse/sort:popular" slot="watchlistButton" shape="pill" size="0.875" theme="transparent-darktext" aria-label="View More Popular TV on Streaming">View More </rt-button></tile-poster-card></carousel-slider></div></section></div><div class="modules-layout" tabindex="-1" data-ModulesNavigationManager="content:focusin"><div id="news-and-guides" class="dom-anchor" data-ModulesNavigationManager="domAnchor"></div><section aria-labelledby="news-and-guides-label" class="news-and-guides" data-adobe-id="news-and-guides" data-qa="section:news-and-guides"><div class="header-wrap"><div class="link-wrap"><h2 class="unset" id="news-and-guides-label"><rt-text size="1.25" style="--textTransform: capitalize;" context="heading" data-qa="title">Related TV News</rt-text></h2><rt-button arialabel="Related TV News" data-qa="view-all-link" href="https://editorial.rottentomatoes.com/more-related-content/?relatedtvseriesid=c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed" shape="pill" size="0.875" style="--buttonPadding: 6px 24px;--backgroundColor: var(--grayLight1);--borderColor: transparent;--letterSpacing: 1px;" theme="light">View All </rt-button></div></div><div class="content-wrap"><carousel-slider tile-width="80%,240px" skeleton="panel" data-qa="carousel"><a slot="tile" href="https://editorial.rottentomatoes.com/article/what-to-expect-in-peacemaker-season-2/" data-qa="article"><tile-dynamic orientation="landscape" skeleton="panel"><rt-img slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2025/08/Peacemaker_S2_Preview-Rep.jpg" loading="lazy"></rt-img><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="article-title">What To Expect In <em>Peacemaker</em>: Season 2</rt-text></drawer-more></tile-dynamic></a><a slot="tile" href="https://editorial.rottentomatoes.com/article/peacemaker-season-2-first-reviews/" data-qa="article"><tile-dynamic orientation="landscape" skeleton="panel"><rt-img slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2025/08/Peacemaker_S2_Reviews-Rep.jpg" loading="lazy"></rt-img><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="article-title"><em>Peacemaker</em>: Season 2 First Reviews: Even Better Than the First Season</rt-text></drawer-more></tile-dynamic></a><a slot="tile" href="https://editorial.rottentomatoes.com/article/6-tv-and-streaming-shows-you-should-binge-watch-in-august-2025/" data-qa="article"><tile-dynamic orientation="landscape" skeleton="panel"><rt-img slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2025/07/600KingOfTheHill.jpg" loading="lazy"></rt-img><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="article-title">6 TV and Streaming Shows You Should Binge-Watch in August</rt-text></drawer-more></tile-dynamic></a></carousel-slider></div></section></div><div class="modules-layout" tabindex="-1" data-ModulesNavigationManager="content:focusin"><div id="videos" class="dom-anchor" data-ModulesNavigationManager="domAnchor"></div><section aria-labelledby="videos-carousel-label" class="videos-carousel" data-adobe-id="videos-carousel" data-qa="section:videos-carousel"><div class="header-wrap"><div class="link-wrap"><h2 class="unset" data-qa="videos-section-title" id="videos-carousel-label"><rt-text size="1.25" context="heading">Videos</rt-text></h2><rt-button arialabel=" videos" data-qa="videos-view-all-link" href="/tv/peacemaker_2022/videos" shape="pill" size="0.875" style="--buttonPadding: 6px 24px;--backgroundColor: var(--grayLight1);--borderColor: transparent;--letterSpacing: 1px;" theme="light">View All </rt-button></div><h3 class="unset"><rt-text context="heading" size="0.75" style="--letterSpacing: 1px; --textColor: var(--grayDark4); --textTransform: capitalize;">Peacemaker </rt-text></h3></div><carousel-slider tile-width="80%,240px" data-VideosCarouselManager="carousel" skeleton="panel" data-qa="videos-carousel"><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="" src="https://resizing.flixster.com/Q9pHL0plKwChI7M2x8OIXc4tTmY=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/166/403/thumb_47CFBBB8-95A3-4634-8B02-581F2C757D98.jpg" alt="Peacemaker: Season 2 Trailer - Weeks Ahead"></rt-img><rt-button theme="transparent" data-ems-id="c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed" data-mpx-id="2447231043621" data-public-id="nTePljVEct61" data-type="TvSeries" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">Peacemaker: Season 2 Trailer - Weeks Ahead</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">Peacemaker: Season 2 Trailer - Weeks Ahead</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">1:39 </rt-badge></tile-video><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="" src="https://resizing.flixster.com/xwQEohhLNSYlXEPpuV8X_yi_hwc=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/206/519/thumb_2CDEC1BA-4D54-4C43-B824-2101B8C0A29D.jpg" alt="Peacemaker: Season 2 Opening Title Sequence"></rt-img><rt-button theme="transparent" data-ems-id="c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed" data-mpx-id="2446199363799" data-public-id="evKz2_ikqufb" data-type="TvSeries" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">Peacemaker: Season 2 Opening Title Sequence</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">Peacemaker: Season 2 Opening Title Sequence</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">1:44 </rt-badge></tile-video><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="" src="https://resizing.flixster.com/uHrBotX26H8cgnZBr_y9pQrDfik=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/959/599/thumb_AE4DD3A1-45A3-463E-8C12-F066951D541A.jpg" alt="Peacemaker: Season 2 Red Band Trailer"></rt-img><rt-button theme="transparent" data-ems-id="c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed" data-mpx-id="2444841539922" data-public-id="LulHILmxo0GT" data-type="TvSeries" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">Peacemaker: Season 2 Red Band Trailer</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">Peacemaker: Season 2 Red Band Trailer</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">1:50 </rt-badge></tile-video><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="" src="https://resizing.flixster.com/6ehG77KgLPJPYWPzW35z3UDrY3c=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/670/763/thumb_D04B0098-73B3-44A9-B14A-E1CC89B500E9.jpg" alt="Peacemaker: Season 2 Comic-Con Trailer"></rt-img><rt-button theme="transparent" data-ems-id="c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed" data-mpx-id="2441317443647" data-public-id="uguhp6w33VWb" data-type="TvSeries" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">Peacemaker: Season 2 Comic-Con Trailer</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">Peacemaker: Season 2 Comic-Con Trailer</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">2:37 </rt-badge></tile-video><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="" src="https://resizing.flixster.com/WN7wehopSKOYXw3sjPGPH7l4Mwg=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/7/359/thumb_35C0690D-5C54-4803-95D0-AEE78E22CDD3.jpg" alt="Peacemaker: Season 2 Teaser"></rt-img><rt-button theme="transparent" data-ems-id="c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed" data-mpx-id="2430958147932" data-public-id="ZOqU2VM5_juu" data-type="TvSeries" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">Peacemaker: Season 2 Teaser</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">Peacemaker: Season 2 Teaser</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">2:12 </rt-badge></tile-video><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="lazy" src="https://resizing.flixster.com/ArY8dUMMJ4Uj3Z_2479ppTxYbI4=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/6/882/thumb_86D17DB7-30AA-4DB3-96D1-387997524FA5.jpg" alt="Peacemaker: Season 2 &#39;Hype Sizzle&#39; Teaser"></rt-img><rt-button theme="transparent" data-ems-id="c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed" data-mpx-id="2430957635557" data-public-id="ucZ_SFM3jCd3" data-type="TvSeries" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">Peacemaker: Season 2 &#39;Hype Sizzle&#39; Teaser</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">Peacemaker: Season 2 &#39;Hype Sizzle&#39; Teaser</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">1:31 </rt-badge></tile-video><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="lazy" src="https://resizing.flixster.com/h8Pwz2jtI_AAsGxT2kEU4A6HHYw=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/606/731/thumb_A33D0BAB-F8A9-43C6-8971-B11DF5642BE0.jpg" alt="Peacemaker: Season 1 Featurette - Behind the Scenes Set Tour"></rt-img><rt-button theme="transparent" data-ems-id="c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed" data-mpx-id="2004237379870" data-public-id="hy7gINV20Xgz" data-type="TvSeries" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">Peacemaker: Season 1 Featurette - Behind the Scenes Set Tour</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">Peacemaker: Season 1 Featurette - Behind the Scenes Set Tour</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">2:25 </rt-badge></tile-video><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="lazy" src="https://resizing.flixster.com/Zf72nYNVLThCE_c3MTZwXQk9O-w=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/187/395/thumb_751C1C8C-ACD5-49BA-862D-584331E8EF42.jpg" alt="Peacemaker: Season 1 Featurette - Opening Credits Behind The Scenes"></rt-img><rt-button theme="transparent" data-ems-id="c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed" data-mpx-id="1995207747775" data-public-id="xR3FXHupihhm" data-type="TvSeries" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">Peacemaker: Season 1 Featurette - Opening Credits Behind The Scenes</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">Peacemaker: Season 1 Featurette - Opening Credits Behind The Scenes</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">1:40 </rt-badge></tile-video><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="lazy" src="https://resizing.flixster.com/5ecIljmgvVUMDcGdo8XdSxLRO_I=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/100/135/thumb_E53011DB-F8C7-4E3C-831B-E950B662F6BC.jpg" alt="Peacemaker: Season 1 Opening Title Sequence"></rt-img><rt-button theme="transparent" data-ems-id="c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed" data-mpx-id="1992968771590" data-public-id="qbI1i6F_q5v2" data-type="TvSeries" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">Peacemaker: Season 1 Opening Title Sequence</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">Peacemaker: Season 1 Opening Title Sequence</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">1:27 </rt-badge></tile-video><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="lazy" src="https://resizing.flixster.com/TgkyPcA__FO7UGZYkLAUzPBzUDI=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/421/735/thumb_871DE5CB-0944-426F-8B7A-48B30F9B4470.jpg" alt="Peacemaker: Season 1 Red Band Trailer"></rt-img><rt-button theme="transparent" data-ems-id="c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed" data-mpx-id="1989011011859" data-public-id="VgqYycz107wr" data-type="TvSeries" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">Peacemaker: Season 1 Red Band Trailer</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">Peacemaker: Season 1 Red Band Trailer</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">2:39 </rt-badge></tile-video><tile-view-more aspect="landscape" background="mediaHero" slot="tile"><rt-button href="/tv/peacemaker_2022/videos" shape="pill" theme="transparent-lighttext">View more videos </rt-button></tile-view-more></carousel-slider></section></div><div class="modules-layout" tabindex="-1" data-ModulesNavigationManager="content:focusin"><div id="photos" class="dom-anchor" data-ModulesNavigationManager="domAnchor"></div><section aria-labelledby="photos-carousel-label" class="photos-carousel" data-adobe-id="photos-carousel" data-qa="section:photos-carousel"><div class="header-wrap"><div class="link-wrap"><h2 class="unset" id="photos-carousel-label"><rt-text size="1.25" context="heading">Photos</rt-text></h2><rt-button arialabel="Peacemaker photos" data-qa="photos-view-all-link" href="/tv/peacemaker_2022/pictures" shape="pill" size="0.875" style="--buttonPadding: 6px 24px;--backgroundColor: var(--grayLight1);--borderColor: transparent;--letterSpacing: 1px;" theme="light">View All </rt-button></div><h3 class="unset"><rt-text context="label" size="0.75" style="--textColor: var(--grayDark4);">Peacemaker </rt-text></h3></div><carousel-slider tile-width="80%,240px" skeleton="panel"><tile-photo data-PhotosCarouselManager="tilePhoto:click" slot="tile" skeleton="panel"><span class="sr-only"></span><rt-img slot="image" loading="" src="https://resizing.flixster.com/pnnbrMa3XqrNasqyxJqaN--H4Cc=/fit-in/352x330/v2/https://resizing.flixster.com/mEY3rZ4CClCMjpOT2WJSWBvf_p8=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvOTY5OTU0MzYtZmM5MC00MmI4LWI1YTEtYjQ4NDU2MWRmZWMzLmpwZw==,https://resizing.flixster.com/AyiRajTpy6lnaHumW_fILpwVRGg=/fit-in/705x460/v2/https://resizing.flixster.com/mEY3rZ4CClCMjpOT2WJSWBvf_p8=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvOTY5OTU0MzYtZmM5MC00MmI4LWI1YTEtYjQ4NDU2MWRmZWMzLmpwZw==" alt="Peacemaker photo 1"></rt-img></tile-photo><tile-photo data-PhotosCarouselManager="tilePhoto:click" slot="tile" skeleton="panel"><span class="sr-only"></span><rt-img slot="image" loading="" src="https://resizing.flixster.com/qE6FP4Bj6VCTuBfe7N66IQTIhRw=/fit-in/352x330/v2/https://resizing.flixster.com/k3cQC7eE0DrdhOhTJ7dknLuhrzk=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvMDE5NGY5NjctZGE2ZC00NWQxLWI0MmUtNGU4ODU1MjNlYzBhLmpwZw==,https://resizing.flixster.com/xAQuYUE2M6oqOo_BeDF9s0Y7J5Y=/fit-in/705x460/v2/https://resizing.flixster.com/k3cQC7eE0DrdhOhTJ7dknLuhrzk=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvMDE5NGY5NjctZGE2ZC00NWQxLWI0MmUtNGU4ODU1MjNlYzBhLmpwZw==" alt="Peacemaker photo 2"></rt-img></tile-photo><tile-photo data-PhotosCarouselManager="tilePhoto:click" slot="tile" skeleton="panel"><span class="sr-only"></span><rt-img slot="image" loading="" src="https://resizing.flixster.com/_3jHOzArNe3oiiVgxP_OQ-cmMHc=/fit-in/352x330/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20861895_b_h9_ag.jpg,https://resizing.flixster.com/EKOMJrRq1EHckFjYDh5SOVSn1vo=/fit-in/705x460/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20861895_b_h9_ag.jpg" alt="Peacemaker photo 3"></rt-img></tile-photo><tile-photo data-PhotosCarouselManager="tilePhoto:click" slot="tile" skeleton="panel"><span class="sr-only"></span><rt-img slot="image" loading="" src="https://resizing.flixster.com/QWAqHtLh7bMuf2TELXEDuAtQ5zM=/fit-in/352x330/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20861895_b_v10_ag.jpg,https://resizing.flixster.com/2tHUgn3tU7-WCqrUQUSir93Gkdk=/fit-in/705x460/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20861895_b_v10_ag.jpg" alt="Peacemaker photo 4"></rt-img></tile-photo><tile-photo data-PhotosCarouselManager="tilePhoto:click" slot="tile" skeleton="panel"><span class="sr-only"></span><rt-img slot="image" loading="" src="https://resizing.flixster.com/2qpZeM1Z2Zs7axXvrcp0rMD26-o=/fit-in/352x330/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20861895_b_h8_ag.jpg,https://resizing.flixster.com/2IaL5BpJB4siKUg4nW6oiaAmqWA=/fit-in/705x460/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20861895_b_h8_ag.jpg" alt="Peacemaker photo 5"></rt-img></tile-photo><tile-photo data-PhotosCarouselManager="tilePhoto:click" slot="tile" skeleton="panel"><span class="sr-only"></span><rt-img slot="image" loading="lazy" src="https://resizing.flixster.com/5TjvqvQvONn4KxuHv07rgFfNjrU=/fit-in/352x330/v2/https://resizing.flixster.com/fwYgrAHcwGp2fRYUl5SKoTnVSzY=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvMjYxOTllNmEtNTg1My00ZDJkLWJmOTQtNjFhOTc4YWY2YTJhLmpwZw==,https://resizing.flixster.com/5HHcytoqlM09VW1dilRz7tpS1t8=/fit-in/705x460/v2/https://resizing.flixster.com/fwYgrAHcwGp2fRYUl5SKoTnVSzY=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvMjYxOTllNmEtNTg1My00ZDJkLWJmOTQtNjFhOTc4YWY2YTJhLmpwZw==" alt="Peacemaker photo 6"></rt-img></tile-photo><tile-photo data-PhotosCarouselManager="tilePhoto:click" slot="tile" skeleton="panel"><span class="sr-only">Peacemaker</span><rt-img slot="image" loading="lazy" src="https://resizing.flixster.com/zNFtUY54pAUCxSEy3Klf2tr3QQI=/fit-in/352x330/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20861895_i_h9_ac.jpg,https://resizing.flixster.com/Gn0OcNgEMPmXB_i-_0whwUrWWr0=/fit-in/705x460/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20861895_i_h9_ac.jpg" alt="Peacemaker"></rt-img></tile-photo><tile-photo data-PhotosCarouselManager="tilePhoto:click" slot="tile" skeleton="panel"><span class="sr-only">Peacemaker</span><rt-img slot="image" loading="lazy" src="https://resizing.flixster.com/ZdM1ETY1IFBSx3tEOi79gBn0aXQ=/fit-in/352x330/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20861895_i_v9_ac.jpg,https://resizing.flixster.com/e2CrSohJjji7pRMIiCRIwlmfDLg=/fit-in/705x460/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20861895_i_v9_ac.jpg" alt="Peacemaker"></rt-img></tile-photo><tile-photo data-PhotosCarouselManager="tilePhoto:click" slot="tile" skeleton="panel"><span class="sr-only">Peacemaker</span><rt-img slot="image" loading="lazy" src="https://resizing.flixster.com/bpwzsf_4hgeeBn5V4TODTxkAuow=/fit-in/352x330/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20861895_i_v8_ac.jpg,https://resizing.flixster.com/z7TWSr_eYCpEotH8_wlxVqshvBk=/fit-in/705x460/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20861895_i_v8_ac.jpg" alt="Peacemaker"></rt-img></tile-photo><tile-photo data-PhotosCarouselManager="tilePhoto:click" slot="tile" skeleton="panel"><span class="sr-only">Peacemaker</span><rt-img slot="image" loading="lazy" src="https://resizing.flixster.com/vSIL4zB3ZG0p9Jnc_3H6y5K7r90=/fit-in/352x330/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20861895_i_h10_ac.jpg,https://resizing.flixster.com/KWIVpOvaUn3tHEP8QNJH5_PgkIA=/fit-in/705x460/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20861895_i_h10_ac.jpg" alt="Peacemaker"></rt-img></tile-photo><tile-view-more aspect="square,landscape" background="mediaHero" slot="tile"><rt-button href="/tv/peacemaker_2022/pictures" shape="pill" theme="transparent-lighttext" aria-label="View more Peacemaker photos">View more photos </rt-button></tile-view-more></carousel-slider><photos-carousel-manager><script id="photosCarousel" type="application/json" hidden>{"title":"Peacemaker","images":[{"aspectRatio":"ASPECT_RATIO_2_3","height":"1920","width":"1296","imageUrl":"https://resizing.flixster.com/AyiRajTpy6lnaHumW_fILpwVRGg=/fit-in/705x460/v2/https://resizing.flixster.com/mEY3rZ4CClCMjpOT2WJSWBvf_p8=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvOTY5OTU0MzYtZmM5MC00MmI4LWI1YTEtYjQ4NDU2MWRmZWMzLmpwZw==","imageUrlMobile":"https://resizing.flixster.com/pnnbrMa3XqrNasqyxJqaN--H4Cc=/fit-in/352x330/v2/https://resizing.flixster.com/mEY3rZ4CClCMjpOT2WJSWBvf_p8=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvOTY5OTU0MzYtZmM5MC00MmI4LWI1YTEtYjQ4NDU2MWRmZWMzLmpwZw==","imageLoading":""},{"aspectRatio":"ASPECT_RATIO_2_3","height":"6000","width":"4050","imageUrl":"https://resizing.flixster.com/xAQuYUE2M6oqOo_BeDF9s0Y7J5Y=/fit-in/705x460/v2/https://resizing.flixster.com/k3cQC7eE0DrdhOhTJ7dknLuhrzk=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvMDE5NGY5NjctZGE2ZC00NWQxLWI0MmUtNGU4ODU1MjNlYzBhLmpwZw==","imageUrlMobile":"https://resizing.flixster.com/qE6FP4Bj6VCTuBfe7N66IQTIhRw=/fit-in/352x330/v2/https://resizing.flixster.com/k3cQC7eE0DrdhOhTJ7dknLuhrzk=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvMDE5NGY5NjctZGE2ZC00NWQxLWI0MmUtNGU4ODU1MjNlYzBhLmpwZw==","imageLoading":""},{"aspectRatio":"ASPECT_RATIO_4_3","height":"1080","width":"1440","imageUrl":"https://resizing.flixster.com/EKOMJrRq1EHckFjYDh5SOVSn1vo=/fit-in/705x460/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20861895_b_h9_ag.jpg","imageUrlMobile":"https://resizing.flixster.com/_3jHOzArNe3oiiVgxP_OQ-cmMHc=/fit-in/352x330/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20861895_b_h9_ag.jpg","imageLoading":""},{"aspectRatio":"ASPECT_RATIO_3_4","height":"2048","width":"1536","imageUrl":"https://resizing.flixster.com/2tHUgn3tU7-WCqrUQUSir93Gkdk=/fit-in/705x460/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20861895_b_v10_ag.jpg","imageUrlMobile":"https://resizing.flixster.com/QWAqHtLh7bMuf2TELXEDuAtQ5zM=/fit-in/352x330/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20861895_b_v10_ag.jpg","imageLoading":""},{"aspectRatio":"ASPECT_RATIO_16_9","height":"2160","width":"3840","imageUrl":"https://resizing.flixster.com/2IaL5BpJB4siKUg4nW6oiaAmqWA=/fit-in/705x460/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20861895_b_h8_ag.jpg","imageUrlMobile":"https://resizing.flixster.com/2qpZeM1Z2Zs7axXvrcp0rMD26-o=/fit-in/352x330/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20861895_b_h8_ag.jpg","imageLoading":""},{"aspectRatio":"ASPECT_RATIO_3_2","height":"1280","width":"1920","imageUrl":"https://resizing.flixster.com/5HHcytoqlM09VW1dilRz7tpS1t8=/fit-in/705x460/v2/https://resizing.flixster.com/fwYgrAHcwGp2fRYUl5SKoTnVSzY=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvMjYxOTllNmEtNTg1My00ZDJkLWJmOTQtNjFhOTc4YWY2YTJhLmpwZw==","imageUrlMobile":"https://resizing.flixster.com/5TjvqvQvONn4KxuHv07rgFfNjrU=/fit-in/352x330/v2/https://resizing.flixster.com/fwYgrAHcwGp2fRYUl5SKoTnVSzY=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvMjYxOTllNmEtNTg1My00ZDJkLWJmOTQtNjFhOTc4YWY2YTJhLmpwZw==","imageLoading":"lazy"},{"aspectRatio":"ASPECT_RATIO_4_3","caption":"Peacemaker","height":"1080","width":"1440","imageUrl":"https://resizing.flixster.com/Gn0OcNgEMPmXB_i-_0whwUrWWr0=/fit-in/705x460/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20861895_i_h9_ac.jpg","imageUrlMobile":"https://resizing.flixster.com/zNFtUY54pAUCxSEy3Klf2tr3QQI=/fit-in/352x330/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20861895_i_h9_ac.jpg","imageLoading":"lazy"},{"aspectRatio":"ASPECT_RATIO_3_4","caption":"Peacemaker","height":"1440","width":"1080","imageUrl":"https://resizing.flixster.com/e2CrSohJjji7pRMIiCRIwlmfDLg=/fit-in/705x460/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20861895_i_v9_ac.jpg","imageUrlMobile":"https://resizing.flixster.com/ZdM1ETY1IFBSx3tEOi79gBn0aXQ=/fit-in/352x330/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20861895_i_v9_ac.jpg","imageLoading":"lazy"},{"aspectRatio":"ASPECT_RATIO_2_3","caption":"Peacemaker","height":"1440","width":"960","imageUrl":"https://resizing.flixster.com/z7TWSr_eYCpEotH8_wlxVqshvBk=/fit-in/705x460/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20861895_i_v8_ac.jpg","imageUrlMobile":"https://resizing.flixster.com/bpwzsf_4hgeeBn5V4TODTxkAuow=/fit-in/352x330/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20861895_i_v8_ac.jpg","imageLoading":"lazy"},{"aspectRatio":"ASPECT_RATIO_16_9","caption":"Peacemaker","height":"1080","width":"1920","imageUrl":"https://resizing.flixster.com/KWIVpOvaUn3tHEP8QNJH5_PgkIA=/fit-in/705x460/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20861895_i_h10_ac.jpg","imageUrlMobile":"https://resizing.flixster.com/vSIL4zB3ZG0p9Jnc_3H6y5K7r90=/fit-in/352x330/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p20861895_i_h10_ac.jpg","imageLoading":"lazy"}],"picturesPageUrl":"/tv/peacemaker_2022/pictures"} </script></photos-carousel-manager></section></div><ad-unit hidden unit-display="mobile" unit-type="mboxadtwo" show-ad-link><div slot="ad-inject" class="rectangle_ad mobile center"></div></ad-unit><ad-unit hidden unit-display="desktop" unit-type="opbannertwo"><div slot="ad-inject" class="banner-ad"></div></ad-unit><div class="modules-layout" tabindex="-1" data-ModulesNavigationManager="content:focusin"><div id="media-info" class="dom-anchor" data-ModulesNavigationManager="domAnchor"></div><section aria-labelledby="media-info-label" class="media-info" data-adobe-id="media-info" data-qa="section:media-info"><div class="header-wrap"><h2 class="unset" id="media-info-label"><rt-text context="heading" size="1.25" style="--textTransform: capitalize;" data-qa="title">Series Info </rt-text></h2></div><div class="content-wrap"><div class="synopsis-wrap"><rt-text class="key" size="0.875" data-qa="synopsis-label">Synopsis</rt-text><rt-text data-qa="synopsis-value">A man fights for peace at any cost, no matter how many people he has to kill to get it.</rt-text></div><dl><div class="category-wrap" data-qa="item"><dt class="key"><rt-text class="key" size="0.875" data-qa="item-label">Executive Producer</rt-text></dt><dd data-qa="item-value-group"><rt-link href="/celebrity/james_gunn" data-qa="item-value">James Gunn</rt-link><rt-text class="delimiter">, </rt-text><rt-link href="/celebrity/peter_safran" data-qa="item-value">Peter Safran</rt-link><rt-text class="delimiter">, </rt-text><rt-link href="/celebrity/john_cena" data-qa="item-value">John Cena</rt-link></dd></div><div class="category-wrap" data-qa="item"><dt class="key"><rt-text class="key" size="0.875" data-qa="item-label">Screenwriter</rt-text></dt><dd data-qa="item-value-group"><rt-link href="/celebrity/james_gunn" data-qa="item-value">James Gunn</rt-link></dd></div><div class="category-wrap" data-qa="item"><dt class="key"><rt-text class="key" size="0.875" data-qa="item-label">Network</rt-text></dt><dd data-qa="item-value-group"><rt-text data-qa="item-value">HBO Max</rt-text></dd></div><div class="category-wrap" data-qa="item"><dt class="key"><rt-text class="key" size="0.875" data-qa="item-label">Rating</rt-text></dt><dd data-qa="item-value-group"><rt-text data-qa="item-value">TV-MA</rt-text></dd></div><div class="category-wrap" data-qa="item"><dt class="key"><rt-text class="key" size="0.875" data-qa="item-label">Genre</rt-text></dt><dd data-qa="item-value-group"><rt-link href="/browse/tv_series_browse/genres:comedy" data-qa="item-value">Comedy</rt-link><rt-text class="delimiter">, </rt-text><rt-link href="/browse/tv_series_browse/genres:action" data-qa="item-value">Action</rt-link></dd></div><div class="category-wrap" data-qa="item"><dt class="key"><rt-text class="key" size="0.875" data-qa="item-label">Original Language</rt-text></dt><dd data-qa="item-value-group"><rt-text data-qa="item-value">English</rt-text></dd></div><div class="category-wrap" data-qa="item"><dt class="key"><rt-text class="key" size="0.875" data-qa="item-label">Release Date</rt-text></dt><dd data-qa="item-value-group"><rt-text data-qa="item-value">Jan 13, 2022</rt-text></dd></div></dl></div></section></div></div><div id="sidebar-wrap"><div data-adobe-id="discovery-sidebar" data-DiscoverySidebarManager="sticky"><discovery-sidebar-manager><script data-json="discoverySidebarJSON" type="application/json">{"mediaType":"tvSeries"}</script></discovery-sidebar-manager><discovery-sidebar skeleton="panel" data-DiscoverySidebarManager="sidebar"></discovery-sidebar><ad-unit data-DiscoverySidebarManager="ad:instantiated" unit-display="desktop" unit-type="topmulti" show-ad-link><div slot="ad-inject"></div></ad-unit></div></div><script id="curation-json" type="application/json">{"emsId":"c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed","rtId":"16913","rtIdTvss":"16913","type":"tvSeries"}</script></div></div><tool-tip data-MediaScorecardManager="tipCritics" hidden><rt-button slot="btnClose" data-MediaScorecardManager="tipCriticsClose:click" theme="transparent" size="1.5"><rt-icon icon="close" image="true"></rt-icon></rt-button><div data-MediaScorecardManager="tipCriticsContent"></div></tool-tip><tool-tip class="component" data-MediaScorecardManager="tipAudience" hidden><rt-button slot="btnClose" data-MediaScorecardManager="tipAudienceClose:click" theme="transparent" size="1.5"><rt-icon icon="close" image="true"></rt-icon></rt-button><div data-MediaScorecardManager="tipAudienceContent"></div></tool-tip><overlay-base data-MediaScorecardManager="overlay:close" hidden><div slot="content"></div></overlay-base><overlay-base data-PhotosCarouselManager="overlayBase:close" hidden><photos-carousel-overlay data-PhotosCarouselManager="photosOverlay:sliderBtnClick" slot="content"><rt-button data-PhotosCarouselManager="closeBtn:click" slot="closeBtn" theme="transparent"><rt-icon icon="close"></rt-icon></rt-button></photos-carousel-overlay></overlay-base><overlay-base data-JwPlayerManager="overlayBase:close" data-VideoPlayerOverlayManager="overlayBase:close,open" hidden><video-player-overlay class="video-overlay-wrap" data-qa="video-overlay" data-VideoPlayerOverlayManager="videoPlayerOverlay:unmute" slot="content"><rt-button data-JwPlayerManager="unmuteBtn:click" slot="unmuteBtn" theme="light"><rt-icon icon="volume-mute-fill"></rt-icon>&ensp; Tap to Unmute </rt-button><div slot="header"><button class="unset transparent" data-VideoPlayerOverlayManager="btnOverlayClose:click" data-qa="video-close-btn"><rt-icon icon="close"><span class="sr-only">Close video</span></rt-icon></button><a class="cta-btn header-cta button hide">See Details</a></div><div slot="content"></div><a slot="footer" class="cta-btn footer-cta button hide">See Details</a></video-player-overlay></overlay-base><div id="video-overlay-player" hidden></div><video-player-overlay-manager></video-player-overlay-manager><jw-player-manager data-AdsVideoSpotlightManager="jwPlayerManager:playlistItem,ready,remove" data-VideoPlayerOverlayManager="jwPlayerManager:playlistItem,pause,ready,relatedClose,relatedOpen"></jw-player-manager><ads-media-scorecard-manager></ads-media-scorecard-manager></div><back-to-top hidden></back-to-top></main><ad-unit hidden unit-display="desktop" unit-type="bottombanner"><div slot="ad-inject" class="sleaderboard_wrapper"></div></ad-unit><ads-global-skin-takeover-manager></ads-global-skin-takeover-manager><footer-manager></footer-manager><footer class="footer container" data-PagePicturesManager="footer"><mobile-app-desktop-footer env="production" hidden></mobile-app-desktop-footer><div class="footer__content-desktop-block" data-qa="footer:section"><div class="footer__content-group"><ul class="footer__links-list"><li class="footer__links-list-item"><a href="/help_desk" data-qa="footer:link-helpdesk">Help</a></li><li class="footer__links-list-item"><a href="/about" data-qa="footer:link-about">About Rotten Tomatoes</a></li><li id="footer-feedback" class="footer__links-list-item" data-qa="footer-feedback-desktop"></li></ul></div><div class="footer__content-group"><ul class="footer__links-list"><li class="footer__links-list-item"><a href="/critics/criteria" data-qa="footer:link-critic-submission">Critic Submission</a></li><li class="footer__links-list-item"><a href="/help_desk/licensing" data-qa="footer:link-licensing">Licensing</a></li><li class="footer__links-list-item"><a href="https://together.nbcuni.com/advertise/?utm_source=rotten_tomatoes&amp;utm_medium=referral&amp;utm_campaign=property_ad_pages&amp;utm_content=footer" target="_blank" rel="noopener" data-qa="footer:link-ads">Advertise With Us</a></li><li class="footer__links-list-item"><a href="//www.fandango.com/careers" target="_blank" rel="noopener" data-qa="footer:link-careers">Careers</a></li></ul></div><div class="footer__content-group footer__newsletter-block"><p class="h3 footer__content-group-title"><rt-icon icon="mail" size="1.25" style="fill:#fff"></rt-icon>&ensp;Join the Newsletter </p><p class="footer__newsletter-copy">Get the freshest reviews, news, and more delivered right to your inbox!</p><rt-button shape="pill" data-FooterManager="btnNewsLetter:click" data-qa="footer-newsletter-desktop">Join The Newsletter </rt-button><a data-FooterManager="linkNewsLetter" class="button footer__newsletter-btn hide" target="_blank" rel="noopener">Join The Newsletter </a></div><div class="footer__content-group footer__social-block" data-qa="footer:social"><p class="h3 footer__content-group-title">Follow Us</p><social-media-icons theme="light" size="20"></social-media-icons></div></div><div class="footer__content-mobile-block" data-qa="mfooter:section"><div class="footer__content-group"><div class="mobile-app-cta-wrap"><mobile-app-cta env="production" showandroid="false" showios="true" hidden></mobile-app-cta></div><p class="footer__copyright-legal" data-qa="mfooter:copyright"><rt-text size="0.75">Copyright &copy; Fandango. All rights reserved.</rt-text></p><p><rt-button shape="pill" data-FooterManager="btnNewsLetter:click" data-qa="footer-newsletter-mobile">Join The Newsletter</rt-button></p><a data-FooterManager="linkNewsLetter" class="button footer__newsletter-btn hide" target="_blank" rel="noopener">Join The Newsletter</a><ul class="footer__links-list list-inline"><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" rel="noopener" data-qa="mfooter:link-privacy-policy">Privacy Policy </a></li><li class="footer__links-list-item"><a href="/policies/terms-and-policies" data-qa="mfooter:link-terms-policies">Terms and Policies</a></li><li class="footer__links-list-item"><img data-FooterManager="iconCCPA" src="https://images.fandango.com/cms/assets/266533e0-7afb-11ed-83f2-4f600722b564--privacyoptions.svg" class="footer__ccpa-icon" loading="lazy" alt="CCPA icon" /><a href="javascript:void(0)" id="ot-sdk-btn" class="ot-sdk-show-settings mobile" data-qa="footer-cookie-settings-mobile">Cookie Settings</a></li><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/privacy/california-consumer-privacy-act" target="_blank" rel="noopener" data-qa="mfooter:link-california-notice">California Notice</a></li><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/privacy/cookies#accordionheader2" target="_blank" rel="noopener" data-qa="mfooter:link-adChoices">Ad Choices</a></li><li id="footer-feedback-mobile" class="footer__links-list-item" data-qa="footer-feedback-mobile"></li><li class="footer__links-list-item"><a href="/faq#accessibility" data-qa="mfooter:link-accessibility">Accessibility</a></li></ul></div></div><div class="footer__copyright"><ul class="footer__links-list list-inline list-inline--separator" data-qa="footer:links-list-privacy"><li class="footer__links-list-item version" data-qa="footer:version"><span>V3.1</span></li><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" rel="noopener" data-qa="footer:link-privacy-policy">Privacy Policy </a></li><li class="footer__links-list-item"><a href="/policies/terms-and-policies" data-qa="footer:link-terms-policies">Terms and Policies</a></li><li class="footer__links-list-item"><img data-FooterManager="iconCCPA" src="https://images.fandango.com/cms/assets/266533e0-7afb-11ed-83f2-4f600722b564--privacyoptions.svg" class="footer__ccpa-icon" loading="lazy" alt="CCPA icon" /><a href="javascript:void(0)" id="ot-sdk-btn" class="ot-sdk-show-settings" data-qa="footer-cookie-settings-desktop">Cookie Settings</a></li><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/privacy/california-consumer-privacy-act" target="_blank" rel="noopener" data-qa="footer:link-california-notice">California Notice</a></li><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/privacy/cookies#accordionheader2" target="_blank" rel="noopener" data-qa="footer:link-adChoices">Ad Choices</a></li><li class="footer__links-list-item"><a href="/faq#accessibility" data-qa="footer:link-accessibility">Accessibility</a></li></ul><span class="footer__copyright-legal" data-qa="footer:copyright">Copyright &copy; Fandango. A Division of <a href="https://www.nbcuniversal.com" target="_blank" rel="noopener" data-qa="footer:link-nbcuniversal">NBCUniversal</a>. All rights reserved. </span></div></footer></div><iframe-container hidden data-ArtiManager="iframeContainer:close,resize" data-iframe-src="https://arti.rottentomatoes.com?theme=iframe" theme="widget"><span slot="logo"><img src="/assets/pizza-pie/images/arti.041d204c4a4.svg" alt="Logo"></img><span>beta</span></span><rt-button aria-label="New chat" data-ArtiManager="btnNewChat:click" id="artiNewChatButton" slot="optBtns" theme="transparent" title="New chat"><rt-icon icon="new-chat" size="1.25" image></rt-icon></rt-button></iframe-container><arti-manager></arti-manager><script type="text/javascript">(function (root){ root.RottenTomatoes || (root.RottenTomatoes={}); root.RottenTomatoes.context || (root.RottenTomatoes.context={}); root.RottenTomatoes.context.resetCookies=["AMCVS_8CF467C25245AE3F0A490D4C%40AdobeOrg", "AMCV_8CF467C25245AE3F0A490D4C%40AdobeOrg", "WRIgnore", "WRUIDAWS", "__CT_Data", "__gads", "_admrla", "_awl", "_cs_c", "_cs_id", "_cs_mk", "_cs_s", "_fbp", "_ga", "_gat_gtmTracker", "_gid", "aam_uuid", "akamai_generated_location", "auth_token", "auth_user", "auth_client", "check", "cognito", "fblo_326803741017", "fbm_326803741017", "fbsr_326803741017", "gpv_Page", "id_token", "is_auth", "loginPlatform", "mbox", "notice_behavior", "optimizelyBuckets", "optimizelyEndUserId", "optimizelyPendingLogEvents", "optimizelySegments", "s_cc", "s_dayslastvisit", "s_dayslastvisit_s", "s_invisit", "s_prevPage", "s_sq", "s_vnum", "cognito", "fbm_326803741017", "fbsr_326803741017", "id_token", "JSESSIONID", "QSI_HistorySession", "QSI_SI_8up4dWDOtjAg0hn_intercept", "_ALGOLIA", "__Host-color-scheme", "__Host-theme-options", "__host_color_scheme", "__host_theme_options", "_cb", "_cb_ls", "_cb_svref", "_chartbeat2", "_chartbeat4", "_chartbeat5", "_sp_id.47f3", "_sp_ses.47f3", "_v__chartbeat3", "adops_master_kvs", "akacd_RTReplatform", "algoliaUT", "cognito", "cl_duid", "fbsr_326803741017", "id_token", "mps_uuid", "session_id", "_admrla", "_awl", "_ga", "_gid", "aam_uuid", "cognito", "fbm_326803741017", "id_token", "_cb", "_cb_ls", "_cb_svref", "_chartbeat2", "adops_master_kvs", "cognito", "id_token", "krg_crb", "krg_uid", "mps_uuid"]; root.Fandango || (root.Fandango={}); root.Fandango.dtmData={ "webVersion": "node", "rtVersion": 3.1, "loggedInStatus": "", "customerId": "", "pageName": "trailers"}; root.RottenTomatoes.criticPage={ "vanity": "david-wilson1", "type": "movies", "typeDisplayName": "Movie", "totalReviews": "", "criticID": "10597"}; root.RottenTomatoes.context.review={ "mediaType": "movie", "title": "No News From God", "emsId": "ef49e96b-6185-3ae1-a768-5ada72678f3d", "type": "all", "sort": undefined, "reviewsCount": 0, "pageInfo": undefined, "reviewerDefaultImg": "https:\u002F\u002Fimages.fandango.com\u002Fcms\u002Fassets\u002F5b6ff500-1663-11ec-ae31-05a670d2d590--rtactordefault.png", "reviewerDefaultImgWidth": "100"}; root.RottenTomatoes.context.useCursorPagination=true; root.RottenTomatoes.context.verifiedTooltip=undefined; root.RottenTomatoes.context.video={ "file": "https:\u002F\u002Flink.theplatform.com\u002Fs\u002FNGweTC\u002Fmedia\u002FumcvJJBe0zN4?formats=MPEG-DASH+widevine,M3U+appleHlsEncryption,M3U+none,MPEG-DASH+none,MPEG4,MP3", "type": "hls", "description": "", "image": "https:\u002F\u002Fstatcdn.fandango.com\u002FMPX\u002Fimage\u002FNBCU_Fandango\u002F433\u002F995\u002Fthumb_249E752F-75BD-4BA4-8EC8-E3BAA4693CA8.jpg", "isRedBand": false, "mediaid": "1105334339671", "mpxId": "1105334339671", "publicId": "umcvJJBe0zN4", "title": "Roman Holiday: Trailer 1", "default": false, "label": "0", "duration": "2:26", "durationInSeconds": "146.313", "emsMediaType": "Movie", "emsId": "a50a127d-e1cb-373d-8f20-4999b7186c77", "overviewPageUrl": "\u002Fm\u002Froman_holiday", "videoPageUrl": "\u002Fm\u002Froman_holiday\u002Fvideos\u002FumcvJJBe0zN4", "videoType": "TRAILER", "adobeDataLayer":{ "content":{ "id": "fandango_1105334339671", "length": "146.313", "type": "vod", "player_name": "jw", "sdk_version": "web: 6.51.0", "channel": "movie", "originator": "paramount pictures", "name": "roman holiday: trailer 1", "rating": "not adult", "stream_type": "video"}, "media_params":{ "genre": "romance, comedy", "show_type": 1}}, "comscore":{ "labelmapping": "c3=\"rottentomatoes.com\", ns_st_st=\"Rotten Tomatoes\", ns_st_pu=\"Paramount Pictures\", ns_st_pr=\"Roman Holiday\", ns_st_sn=\"*null\", ns_st_en=\"*null\", ns_st_ge=\"Romance,Comedy\", ns_st_ia=\"0\", ns_st_ce=\"0\", ns_st_ddt=\"1953\", ns_st_tdt=\"1953\""}, "thumbnail": "https:\u002F\u002Fresizing.flixster.com\u002F-3W0tBGrEVaKaECTa6BWAMP_J0Y=\u002F270x160\u002Fv2\u002Fhttps:\u002F\u002Fstatcdn.fandango.com\u002FMPX\u002Fimage\u002FNBCU_Fandango\u002F433\u002F995\u002Fthumb_249E752F-75BD-4BA4-8EC8-E3BAA4693CA8.jpg"}; root.RottenTomatoes.context.videoClipsJson={ "count": 11}; root.RottenTomatoes.context.layout={ "header":{ "movies":{ "moviesAtHome":{ "tarsSlug": "rt-nav-movies-at-home", "linkList": [{ "header": "Fandango at Home", "slug": "fandango-at-home-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Faffiliates:fandango-at-home"},{ "header": "Peacock", "slug": "peacock-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Faffiliates:peacock"},{ "header": "Netflix", "slug": "netflix-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Faffiliates:netflix"},{ "header": "Apple TV+", "slug": "apple-tv-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Faffiliates:apple-tv-plus"},{ "header": "Prime Video", "slug": "prime-video-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Faffiliates:prime-video"},{ "header": "Most Popular Streaming movies", "slug": "most-popular-streaming-movies-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Fsort:popular"},{ "header": "Certified Fresh movies", "slug": "certified-fresh-movies-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Fcritics:certified_fresh"},{ "header": "Browse all", "slug": "browse-all-link", "url": "\u002Fbrowse\u002Fmovies_at_home"}]}}, "editorial":{ "guides":{ "posts": [{ "ID": 161109, "author": 12, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2023\u002F09\u002F600EssentialFootballMovies.png"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fguide\u002Fbest-football-movies\u002F", "status": "publish", "title": "59 Best Football Movies, Ranked by Tomatometer", "type": "guide"},{ "ID": 253470, "author": 12, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2025\u002F08\u002FBest_New_Romcoms600.jpg"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fguide\u002Fbest-new-rom-coms-romance-movies\u002F", "status": "publish", "title": "50 Best New Rom-Coms and Romance Movies", "type": "guide"}], "title": "Guides", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fcountdown\u002F"}, "hubs":{ "posts": [{ "ID": 237626, "author": 12, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2023\u002F05\u002FRT_WTW_Generic_2023_Thumbnail_600x314_021623.jpg"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Frt-hub\u002Fwhat-to-watch\u002F", "status": "publish", "title": "What to Watch: In Theaters and On Streaming", "type": "rt-hub"},{ "ID": 140214, "author": 12, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2023\u002F02\u002FRT_AwardsTour_Thumbnail_600x314.jpg"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Frt-hub\u002Fawards-tour\u002F", "status": "publish", "title": "Awards Tour", "type": "rt-hub"}], "title": "Hubs", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Frt-hubs\u002F"}, "news":{ "posts": [{ "ID": 273082, "author": 79, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2025\u002F08\u002FNew_Streaming_September_2025-Rep.jpg"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002Fnew-movies-and-shows-streaming-in-september-2025-what-to-watch-on-netflix-prime-video-hbo-max-disney-and-more\u002F", "status": "publish", "title": "New Movies and Shows Streaming in September: What to watch on Netflix, Prime Video, HBO Max, Disney+ and More", "type": "article"},{ "ID": 273326, "author": 669, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2025\u002F09\u002FConjuring_Last_Rites_Reviews-Rep.jpg"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002Fthe-conjuring-last-rites-first-reviews\u002F", "status": "publish", "title": "\u003Cem\u003EThe Conjuring: Last Rites\u003C\u002Fem\u003E First Reviews: A Frightful, Fitting Send-off", "type": "article"}], "title": "RT News", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fnews\u002F"}}, "trendingTarsSlug": "rt-nav-trending", "trending": [{ "header": "Emmy Noms", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002F2025-emmys-ballot-complete-with-tomatometer-and-popcornmeter-scores\u002F"},{ "header": "Re-Release Calendar", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002Fmovie-re-releases-calendar\u002F"},{ "header": "Renewed and Cancelled TV", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002Frenewed-and-cancelled-tv-shows-2025\u002F"},{ "header": "The Rotten Tomatoes App ", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002Fapp\u002F"}], "certifiedMedia":{ "certifiedFreshTvSeason":{ "header": null, "media":{ "url": "\u002Ftv\u002Fthe_paper_2025\u002Fs01", "name": "The Paper: Season 1", "score": 83, "posterImg": "https:\u002F\u002Fresizing.flixster.com\u002FyFijQcjPYUWUelgmiZLHgkXU7hw=\u002F206x305\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FDFkkHf5pEVX_apKtIQZcoEvI6RU=\u002Ffit-in\u002F180x240\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FtexEZJLAG-KcVpfCdkT2R1t4cmE=\u002Fems.cHJkLWVtcy1hc3NldHMvdHZzZWFzb24vYTM3OWM2MTctN2M3Ny00MjdhLTk4NDUtODE5ZWUwMWExNGRhLnBuZw=="}, "tarsSlug": "rt-nav-list-cf-picks"}, "certifiedFreshMovieInTheater":{ "header": null, "media":{ "url": "\u002Fm\u002Ftwinless", "name": "Twinless", "score": 98, "posterImg": "https:\u002F\u002Fresizing.flixster.com\u002Fj7lw2KeY9_XyfZQdqRZGku7_9C8=\u002F206x305\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FuxoeWz7uWmeYIV94_SzEV_osqe4=\u002Ffit-in\u002F180x240\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FVlylB3xT2RIYmRivMx37O3yD76Q=\u002Fems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2ZlNDQ1MGQ5LTFjN2QtNDIwNC04NWE1LTM5NGM4N2U5ZTgzYy5qcGc="}}, "certifiedFreshMovieInTheater4":{ "header": null, "media":{ "url": "\u002Fm\u002Fhamilton_2020", "name": "Hamilton", "score": 98, "posterImg": "https:\u002F\u002Fresizing.flixster.com\u002F1woquJmQfEhWCZtm7GcH0NMHsYA=\u002F206x305\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FPeAJ5ZpF5qB98ZiX6ixNDCgW2P0=\u002Ffit-in\u002F180x240\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FVmBvlTk8-z7pQvDZXTgSdj93WDE=\u002Fems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzkzY2IxZjFkLTE1NjEtNDQ4Yi05NDY3LTcxNzFmMDVhMDczNi5qcGc="}}, "certifiedFreshMovieAtHome":{ "header": null, "media":{ "url": "\u002Fm\u002Fthe_thursday_murder_club", "name": "The Thursday Murder Club", "score": 76, "posterImg": "https:\u002F\u002Fresizing.flixster.com\u002FjeeldFGcfSMgG09ey5VB7TCFiek=\u002F206x305\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002F9LXDkCzIBBNEiPURkB9t6VefF5Q=\u002Ffit-in\u002F180x240\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FrwdeR5xIiN0k7SWr6yXdnmb6zP8=\u002Fems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2EzYWFkZWJiLWE5N2MtNDc3MS1iMDRlLTk0YWVlYzI5M2UxZS5qcGc="}}, "tarsSlug": "rt-nav-list-cf-picks"}, "tvLists":{ "newTvTonight":{ "tarsSlug": "rt-hp-text-list-3", "title": "New TV Tonight", "shows": [{ "title": "Task: Season 1", "tomatometer":{ "tomatometer": 89, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Ftask\u002Fs01"},{ "title": "The Walking Dead: Daryl Dixon: Season 3", "tomatometer":{ "tomatometer": 80, "sentiment": "positive", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fthe_walking_dead_daryl_dixon\u002Fs03"},{ "title": "The Crow Girl: Season 1", "tomatometer":{ "tomatometer": 80, "sentiment": "positive", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fthe_crow_girl\u002Fs01"},{ "title": "Only Murders in the Building: Season 5", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fonly_murders_in_the_building\u002Fs05"},{ "title": "The Girlfriend: Season 1", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fthe_girlfriend\u002Fs01"},{ "title": "aka Charlie Sheen: Season 1", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Faka_charlie_sheen\u002Fs01"},{ "title": "Wizards Beyond Waverly Place: Season 2", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fwizards_beyond_waverly_place\u002Fs02"},{ "title": "Seen & Heard: the History of Black Television: Season 1", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fseen_and_heard_the_history_of_black_television\u002Fs01"},{ "title": "The Fragrant Flower Blooms With Dignity: Season 1", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fthe_fragrant_flower_blooms_with_dignity\u002Fs01"},{ "title": "Guts & Glory: Season 1", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fguts_and_glory\u002Fs01"}]}, "mostPopularTvOnRt":{ "tarsSlug": "rt-hp-text-list-2", "title": "Most Popular TV on RT", "shows": [{ "title": "The Paper: Season 1", "tomatometer":{ "tomatometer": 83, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fthe_paper_2025\u002Fs01"},{ "title": "Dexter: Resurrection: Season 1", "tomatometer":{ "tomatometer": 95, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fdexter_resurrection\u002Fs01"},{ "title": "Alien: Earth: Season 1", "tomatometer":{ "tomatometer": 95, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Falien_earth\u002Fs01"},{ "title": "Task: Season 1", "tomatometer":{ "tomatometer": 89, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Ftask\u002Fs01"},{ "title": "Wednesday: Season 2", "tomatometer":{ "tomatometer": 87, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fwednesday\u002Fs02"},{ "title": "Peacemaker: Season 2", "tomatometer":{ "tomatometer": 99, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fpeacemaker_2022\u002Fs02"},{ "title": "The Terminal List: Dark Wolf: Season 1", "tomatometer":{ "tomatometer": 73, "sentiment": "positive", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fthe_terminal_list_dark_wolf\u002Fs01"},{ "title": "Hostage: Season 1", "tomatometer":{ "tomatometer": 82, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fhostage_2025\u002Fs01"},{ "title": "Chief of War: Season 1", "tomatometer":{ "tomatometer": 93, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fchief_of_war\u002Fs01"},{ "title": "Irish Blood: Season 1", "tomatometer":{ "tomatometer": 100, "sentiment": "positive", "certified": false}, "tvPageUrl": "\u002Ftv\u002Firish_blood\u002Fs01"}]}}}, "links":{ "moviesInTheaters":{ "certifiedFresh": "\u002Fbrowse\u002Fmovies_in_theaters\u002Fcritics:certified_fresh~sort:popular", "comingSoon": "\u002Fbrowse\u002Fmovies_coming_soon\u002F", "openingThisWeek": "\u002Fbrowse\u002Fmovies_in_theaters\u002Fsort:newest", "title": "\u002Fbrowse\u002Fmovies_in_theaters", "topBoxOffice": "\u002Fbrowse\u002Fmovies_in_theaters"}, "onDvdAndStreaming":{ "all": "\u002Fbrowse\u002Fmovies_at_home\u002F", "certifiedFresh": "\u002Fbrowse\u002Fmovies_at_home\u002Fcritics:certified_fresh", "title": "\u002Fbrowse\u002Fmovies_at_home\u002F", "top": "\u002Fbrowse\u002Fmovies_at_home\u002Fsort:popular"}, "moreMovies":{ "topMovies": "\u002Fbrowse\u002Fmovies_at_home\u002Fsort:popular", "trailers": "\u002Ftrailers"}, "tvTonight": "\u002Fbrowse\u002Ftv_series_browse\u002Fsort:newest", "tvPopular": "\u002Fbrowse\u002Ftv_series_browse\u002Fsort:popular", "moreTv":{ "topTv": "\u002Fbrowse\u002Ftv_series_browse\u002Fsort:popular", "certifiedFresh": "\u002Fbrowse\u002Ftv_series_browse\u002Fcritics:fresh"}, "editorial":{ "allTimeLists": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fall-time-lists\u002F", "bingeGuide": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fbinge-guide\u002F", "comicsOnTv": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fcomics-on-tv\u002F", "countdown": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fcountdown\u002F", "fiveFavoriteFilms": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Ffive-favorite-films\u002F", "videoInterviews": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fvideo-interviews\u002F", "weekendBoxOffice": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fweekend-box-office\u002F", "weeklyKetchup": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fweekly-ketchup\u002F", "whatToWatch": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwhat-to-watch\u002F"}, "advertisingFooter": "https:\u002F\u002Ftogether.nbcuni.com\u002Fadvertise\u002F?utm_source=rotten_tomatoes&utm_medium=referral&utm_campaign=property_ad_pages&utm_content=footer", "californiaNotice": "https:\u002F\u002Fwww.nbcuniversalprivacy.com\u002Fprivacy\u002Fcalifornia-consumer-privacy-act", "careers": "\u002F\u002Fwww.fandango.com\u002Fcareers", "cookieManagement": "https:\u002F\u002Fwww.nbcuniversalprivacy.com\u002Fprivacy\u002Fcookies#accordionheader2", "fandangoAbout": "https:\u002F\u002Fwww.fandango.com\u002Fabout-us", "privacyPolicy": "https:\u002F\u002Fwww.nbcuniversalprivacy.com\u002Ffandango-privacy-policy", "termsPolicies": "\u002Fpolicies\u002Fterms-and-policies"}}; root.RottenTomatoes.thirdParty={ "chartBeat":{ "auth": "64558", "domain": "rottentomatoes.com"}, "mpx":{ "accountPid": "NGweTC", "playerPid": "y__7B0iQTi4P", "playerPidPDK6": "pdk6_y__7B0iQTi4P", "accountId": "2474312077"}, "algoliaSearch":{ "aId": "79FRDP12PN", "sId": "175588f6e5f8319b27702e4cc4013561"}, "cognito":{ "upId": "us-west-2_4L0ZX4b1U", "clientId": "7pu48v8i2n25t4vhes0edck31c"}}; root.RottenTomatoes.serviceWorker={ "isServiceWokerOn": true}; root.__RT__ || (root.__RT__={}); root.__RT__.featureFlags={ "adsCarouselHP": false, "adsCarouselHPSlug": "rt-sponsored-carousel-list-mcdonalds-hp", "adsCarouselOP": false, "adsCarouselOPSlug": "rt-sponsored-carousel-list-mcdonalds-op", "adsMockDLP": false, "adsPages": "none", "adsSponsoredOverrideOP": true, "adsSponsoredOverrideOPSlugs": "rt-sponsored-override-op-starz", "adsVideoSpotlightHP": false, "appleSigninEnabled": true, "artiEnabled": true, "authPasswordEnabled": true, "authVerboseLogs": false, "bypassCriticValidationEnabled": false, "castAndCrewEnabled": true, "cookieConsentServiceEnabled": false, "crssoEnabled": false, "editorialApiDisabled": false, "faqUpdatesEnabled": true, "legacyBridge": true, "logVerboseEnabled": false, "mobileAppAndroid": "https:\u002F\u002Fplay.google.com\u002Fstore\u002Fapps\u002Fdetails?id=com.rottentomatoes.android", "mobileAppIos": "https:\u002F\u002Fapps.apple.com\u002Fus\u002Fapp\u002Frotten-tomatoes-movies-tv\u002Fid6673916573", "mobileAppIosMeta": "app-id=6673916573, app-argument=https:\u002F\u002Fwww.rottentomatoes.com\u002F", "mobileNavEnabled": true, "oneTrustJwtApiUrl": "https:\u002F\u002Fonetrustjwt.services.fandango.com", "oneTrustJwtServiceEnabled": false, "pageJsonEnabled": false, "profilesFeaturesEnabled": false, "profilesUsernameEnabled": false, "redesignMediaHeroEnabled": true, "redesignMoreLikeThis": true, "redesignSortTable": true, "trafficAndroidEnabled": false, "trafficSafariEnabled": true, "userMigrationEnabled": true, "versantFreewheelEnabled": false, "versantMpsDomain": "app.mps.vsnt.net", "versantMpsEnabled": false, "versantOneTrustScriptBlock": "\u003C!-- OneTrust Cookies Consent Notice start for rottentomatoes.com --\u003E \u003Cscript src=\"https:\u002F\u002Fcdn.cookielaw.org\u002Fconsent\u002F01978557-1604-76a7-ad7c-18216757cf52-test\u002FotSDKStub.js\" type=\"text\u002Fjavascript\" charset=\"UTF-8\" data-domain-script=\"01978557-1604-76a7-ad7c-18216757cf52-test\" integrity=\"sha384-Exfxdyaw5OnsUlHEKlNlz7OwgVCyLlitAtJsDmSNh3LeLlCjWXos3X\u002FCMNUbQ\u002FgA\" crossorigin=\"anonymous\" \u003E\u003C\u002Fscript\u003E \u003Cscript type=\"text\u002Fjavascript\"\u003E function OptanonWrapper(){ if (OnetrustActiveGroups.includes('7')){ document.querySelector('search-results-nav-manager')?.setAlgoliaInsightUserToken?.();}} \u003C\u002Fscript\u003E \u003C!-- OneTrust Cookies Consent Notice end for rottentomatoes.com --\u003E \u003C!-- OneTrust IAB US Privacy (USP) --\u003E \u003Cscript src=\"https:\u002F\u002Fcdn.cookielaw.org\u002Fopt-out\u002FotCCPAiab.js\" id=\"privacyCookie\" type=\"text\u002Fjavascript\" charset=\"UTF-8\" ccpa-opt-out-ids=\"USP\" ccpa-opt-out-geo=\"US\" ccpa-opt-out-lspa=\"false\"\u003E\u003C\u002Fscript\u003E \u003C!-- OneTrust IAB US Privacy (USP) end --\u003E", "videoGeoFencingEnabled": true}; root.RottenTomatoes.context.adsMockDLP=false; root.RottenTomatoes.context.req={ "params":{ "vanity": "peacemaker_2022"}, "query":{}, "route":{}, "url": "\u002Ftv\u002Fpeacemaker_2022", "secure": false, "buildVersion": undefined}; root.RottenTomatoes.context.config={}; root.BK={ "PageName": "http:\u002F\u002Fwww.rottentomatoes.com\u002Ftv\u002Fpeacemaker_2022", "SiteID": 37528, "SiteSection": "tv", "TvSeriesTitle": "Peacemaker", "TvSeriesId": "c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed"}; root.RottenTomatoes.dtmData={ "customerId": "", "loggedInStatus": "", "rtVersion": 3.1, "webVersion": "node", "emsID": "c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed", "Series Title": "Peacemaker (2022)", "pageName": "rt | tv | series | Peacemaker (2022)", "titleGenre": "Comedy", "titleId": "c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed", "titleName": "Peacemaker (2022)", "titleType": "Tv"}; root.RottenTomatoes.context.gptSite="tv";}(this)); </script><script fetchpriority="high" src="/assets/pizza-pie/javascripts/bundles/roma/preload.18bcfff8e54.js"></script><script src="/assets/pizza-pie/javascripts/bundles/roma/vendors.a4cc402b78a.js"></script><script src="/assets/pizza-pie/javascripts/bundles/roma/default.24dc1977289.js"></script><script async data-SearchResultsNavManager="script:load" src="https://cdn.jsdelivr.net/npm/algoliasearch@4/dist/algoliasearch-lite.umd.js"></script><script src="/assets/pizza-pie/javascripts/templates/roma/searchNav.a3288ea5efe.js"></script><script src="/assets/pizza-pie/javascripts/bundles/roma/searchNav.6a836b4ca81.js"></script><script src="/assets/pizza-pie/javascripts/templates/pages/tvSeries/index.cef0f7dcfdd.js"></script><script src="/assets/pizza-pie/javascripts/bundles/pages/tvSeries/index.8bffb4ba4ad.js"></script><script>if (window.mps && typeof window.mps.writeFooter==='function'){ window.mps.writeFooter();} </script><script>window._satellite && _satellite.pageBottom(); </script></body></html> 1 + <!DOCTYPE html><html lang="en" dir="ltr" xmlns="http://www.w3.org/1999/xhtml" prefix="fb: http://www.facebook.com/2008/fbml og: http://opengraphprotocol.org/schema/"><head prefix="og: http://ogp.me/ns# flixstertomatoes: http://ogp.me/ns/apps/flixstertomatoes#"><script charset="UTF-8" crossorigin="anonymous" data-domain-script="7e979733-6841-4fce-9182-515fac69187f" integrity="sha384-TKdmlzVmoD70HzftTw4WtOzIBL5mNx8mXSRzEvwrWjpIJ7FZ/EuX758yMDWXtRUN" src="https://cdn.cookielaw.org/consent/7e979733-6841-4fce-9182-515fac69187f/otSDKStub.js" type="text/javascript"></script><script type="text/javascript">function OptanonWrapper(){ if (OnetrustActiveGroups.includes('7')){ document.querySelector('search-results-nav-manager')?.setAlgoliaInsightUserToken?.()}} </script><script ccpa-opt-out-ids="USP" ccpa-opt-out-geo="US" ccpa-opt-out-lspa="false" charset="UTF-8" src="https://cdn.cookielaw.org/opt-out/otCCPAiab.js" type="text/javascript"></script><script src="/assets/pizza-pie/javascripts/bundles/roma/rt-common.js?single"></script><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><meta http-equiv="x-ua-compatible" content="ie=edge"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="shortcut icon" sizes="76x76" type="image/x-icon" href="https://www.rottentomatoes.com/assets/pizza-pie/images/favicon.ico" /><title>Peacemaker (2022) | Rotten Tomatoes</title><meta name="description" content="Discover reviews, ratings, and trailers for Peacemaker (2022) on Rotten Tomatoes. Stay updated with critic and audience scores today!" /><meta name="twitter:card" content="summary" /><meta name="twitter:image" content="https://resizing.flixster.com/UHglta_RX5_h8fsHS2BljZkvfZk=/206x305/v2/https://resizing.flixster.com/mEY3rZ4CClCMjpOT2WJSWBvf_p8=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvOTY5OTU0MzYtZmM5MC00MmI4LWI1YTEtYjQ4NDU2MWRmZWMzLmpwZw==" /><meta name="twitter:title" content="Peacemaker (2022) | Rotten Tomatoes" /><meta name="twitter:text:title" content="Peacemaker (2022) | Rotten Tomatoes" /><meta name="twitter:description" content="Discover reviews, ratings, and trailers for Peacemaker (2022) on Rotten Tomatoes. Stay updated with critic and audience scores today!" /><meta property="og:site_name" content="Rotten Tomatoes" /><meta property="og:title" content="Peacemaker (2022) | Rotten Tomatoes" /><meta property="og:description" content="Discover reviews, ratings, and trailers for Peacemaker (2022) on Rotten Tomatoes. Stay updated with critic and audience scores today!" /><meta property="og:type" content="video.tv_show" /><meta property="og:url" content="https://www.rottentomatoes.com/tv/peacemaker_2022" /><meta property="og:image" content="https://resizing.flixster.com/UHglta_RX5_h8fsHS2BljZkvfZk=/206x305/v2/https://resizing.flixster.com/mEY3rZ4CClCMjpOT2WJSWBvf_p8=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvOTY5OTU0MzYtZmM5MC00MmI4LWI1YTEtYjQ4NDU2MWRmZWMzLmpwZw==" /><meta property="og:locale" content="en_US" /><link rel="canonical" href="https://www.rottentomatoes.com/tv/peacemaker_2022" /><script type="application/ld+json">{"@context":"http://schema.org","@type":"TVSeries","actor":[{"@type":"Person","name":"John Cena","sameAs":"https://www.rottentomatoes.com/celebrity/john_cena","image":"https://resizing.flixster.com/qFr2ZK1qYDkqSmM5eT3nz_n6E_g=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/487578_v9_ba.jpg"},{"@type":"Person","name":"Danielle Brooks","sameAs":"https://www.rottentomatoes.com/celebrity/danielle_brooks","image":"https://resizing.flixster.com/KhnY5vsfjM0vtw0cZL3aNxXbeUE=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/765589_v9_bc.jpg"},{"@type":"Person","name":"Freddie Stroma","sameAs":"https://www.rottentomatoes.com/celebrity/freddie_stroma","image":"https://resizing.flixster.com/Yk2eiDCtamfmNlK-xMa7nmEw_Po=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/GNLZZGG00283ZZD.jpg"},{"@type":"Person","name":"Chukwudi Iwuji","sameAs":"https://www.rottentomatoes.com/celebrity/chukwudi_iwuji","image":"https://resizing.flixster.com/uNAFlG9dNMjJwyMbPDiCsbjkX8I=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/565157_v9_ba.jpg"},{"@type":"Person","name":"Jennifer Holland","sameAs":"https://www.rottentomatoes.com/celebrity/jennifer_holland","image":"https://resizing.flixster.com/-xeYAf0O7fGIQHRx_YkL7vnaMMg=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/331642_v9_bb.jpg"},{"@type":"Person","name":"Steve Agee","sameAs":"https://www.rottentomatoes.com/celebrity/steve_agee","image":"https://resizing.flixster.com/YprPSg0SXNIqq-Wy4UEz4ovBnOw=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/223358_v9_bd.jpg"}],"aggregateRating":{"@type":"AggregateRating","bestRating":"100","description":"The Tomatometer rating โ€“ based on the published opinions of hundreds of film and television critics โ€“ is a trusted measurement of movie and TV programming quality for millions of moviegoers. It represents the percentage of professional critic reviews that are positive for a given film or television show.","name":"Tomatometer","ratingCount":150,"ratingValue":"96","reviewCount":150,"worstRating":"0"},"containsSeason":[{"@type":"TVSeason","name":"Season 2","url":"https://www.rottentomatoes.com/tv/peacemaker_2022/s02"},{"@type":"TVSeason","name":"Season 1","url":"https://www.rottentomatoes.com/tv/peacemaker_2022/s01"}],"contentRating":"TV-MA","dateCreated":"2022-01-13","description":"Discover reviews, ratings, and trailers for Peacemaker (2022) on Rotten Tomatoes. Stay updated with critic and audience scores today!","genre":["Comedy","Action"],"image":"https://resizing.flixster.com/mEY3rZ4CClCMjpOT2WJSWBvf_p8=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvOTY5OTU0MzYtZmM5MC00MmI4LWI1YTEtYjQ4NDU2MWRmZWMzLmpwZw==","name":"Peacemaker (2022)","numberOfSeasons":2,"partOfSeries":{"@type":"TVSeries","name":"Peacemaker (2022)","startDate":"2022-01-13","url":"https://www.rottentomatoes.com/tv/peacemaker_2022"},"producer":[{"@type":"Person","name":"James Gunn","sameAs":"https://www.rottentomatoes.com/celebrity/james_gunn","image":"https://images.fandango.com/cms/assets/b0cefeb0-b6a8-11ed-81d8-51a487a38835--poster-default-thumbnail.jpg"},{"@type":"Person","name":"Peter Safran","sameAs":"https://www.rottentomatoes.com/celebrity/peter_safran","image":"https://images.fandango.com/cms/assets/b0cefeb0-b6a8-11ed-81d8-51a487a38835--poster-default-thumbnail.jpg"}],"url":"https://www.rottentomatoes.com/tv/peacemaker_2022","video":{"@type":"VideoObject","thumbnailUrl":"https://statcdn.fandango.com/MPX/image/NBCU_Fandango/166/403/thumb_47CFBBB8-95A3-4634-8B02-581F2C757D98.jpg","name":"Peacemaker: Season 2 Trailer - Weeks Ahead","duration":"1:39","sourceOrganization":"MPX","uploadDate":"2025-08-28T16:36:57","description":"","contentUrl":"https://www.rottentomatoes.com/tv/peacemaker_2022/videos/nTePljVEct61"}}</script></head><body class="body no-touch js-mptd-layout" data-AdsGlobalSkinTakeoverManager="body" data-SearchResultsNavManager="body"><div id="main" class="container rt-layout__body"><main id="main_container" class="container rt-layout__content"></main></div></body></html>
+1 -1
internal/services/samples/series_season.html
··· 1 - <!DOCTYPE html><html lang="en" dir="ltr" xmlns="http://www.w3.org/1999/xhtml" prefix="fb: http://www.facebook.com/2008/fbml og: http://opengraphprotocol.org/schema/"><head prefix="og: http://ogp.me/ns# flixstertomatoes: http://ogp.me/ns/apps/flixstertomatoes#"><script charset="UTF-8" crossorigin="anonymous" data-domain-script="7e979733-6841-4fce-9182-515fac69187f" integrity="sha384-TKdmlzVmoD70HzftTw4WtOzIBL5mNx8mXSRzEvwrWjpIJ7FZ/EuX758yMDWXtRUN" src="https://cdn.cookielaw.org/consent/7e979733-6841-4fce-9182-515fac69187f/otSDKStub.js" type="text/javascript"></script><script type="text/javascript">function OptanonWrapper(){ if (OnetrustActiveGroups.includes('7')){ document.querySelector('search-results-nav-manager')?.setAlgoliaInsightUserToken?.();}} </script><script ccpa-opt-out-ids="USP" ccpa-opt-out-geo="US" ccpa-opt-out-lspa="false" charset="UTF-8" src="https://cdn.cookielaw.org/opt-out/otCCPAiab.js" type="text/javascript"></script><script src="/assets/pizza-pie/javascripts/bundles/roma/rt-common.js?single"></script><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><meta http-equiv="x-ua-compatible" content="ie=edge"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="shortcut icon" sizes="76x76" type="image/x-icon" href="https://www.rottentomatoes.com/assets/pizza-pie/images/favicon.ico" /><title>Peacemaker: Season 2 | Rotten Tomatoes</title><meta name="description" content="Discover reviews, ratings, and trailers for Peacemaker: Season 2 on Rotten Tomatoes. Stay updated with critic and audience scores today!" /><meta name="twitter:card" content="summary" /><meta name="twitter:image" content="https://resizing.flixster.com/icqaMuFdXKqmN8qur-dfG4I-hWs=/206x305/v2/https://resizing.flixster.com/J-WXxwHSdv6w7VHxcCrPdebOvUA=/ems.cHJkLWVtcy1hc3NldHMvdHZzZWFzb24vNmE4MmUxOWItNGY5Yy00YzY0LTk5ODktZDY1MGEzZjdmZDFhLmpwZw==" /><meta name="twitter:title" content="Peacemaker: Season 2 | Rotten Tomatoes" /><meta name="twitter:text:title" content="Peacemaker: Season 2 | Rotten Tomatoes" /><meta name="twitter:description" content="Discover reviews, ratings, and trailers for Peacemaker: Season 2 on Rotten Tomatoes. Stay updated with critic and audience scores today!" /><meta property="og:site_name" content="Rotten Tomatoes" /><meta property="og:title" content="Peacemaker: Season 2 | Rotten Tomatoes" /><meta property="og:description" content="Discover reviews, ratings, and trailers for Peacemaker: Season 2 on Rotten Tomatoes. Stay updated with critic and audience scores today!" /><meta property="og:type" content="video.tv_show" /><meta property="og:url" content="https://www.rottentomatoes.com/tv/peacemaker_2022/s02" /><meta property="og:image" content="https://resizing.flixster.com/icqaMuFdXKqmN8qur-dfG4I-hWs=/206x305/v2/https://resizing.flixster.com/J-WXxwHSdv6w7VHxcCrPdebOvUA=/ems.cHJkLWVtcy1hc3NldHMvdHZzZWFzb24vNmE4MmUxOWItNGY5Yy00YzY0LTk5ODktZDY1MGEzZjdmZDFhLmpwZw==" /><meta property="og:locale" content="en_US" /><link rel="canonical" href="https://www.rottentomatoes.com/tv/peacemaker_2022/s02" /><script>var dataLayer=dataLayer || []; var RottenTomatoes=RottenTomatoes ||{}; RottenTomatoes.dtmData={ "customerId": "", "loggedInStatus": "", "rtVersion": 3.1, "webVersion": "node", "emsID": "76add34d-b98d-34b5-9e0f-2eac74d2ab10", "Season Title": "Season 2", "Series Title": "Peacemaker", "pageName": "rt | tv | season | Peacemaker | Season 2", "titleGenre": "Comedy", "titleId": "76add34d-b98d-34b5-9e0f-2eac74d2ab10", "titleName": "Peacemaker", "titleType": "Tv"}; dataLayer.push({ "customerId": "", "loggedInStatus": "", "rtVersion": 3.1, "webVersion": "node", "emsID": "76add34d-b98d-34b5-9e0f-2eac74d2ab10", "Season Title": "Season 2", "Series Title": "Peacemaker", "pageName": "rt | tv | season | Peacemaker | Season 2", "titleGenre": "Comedy", "titleId": "76add34d-b98d-34b5-9e0f-2eac74d2ab10", "titleName": "Peacemaker", "titleType": "Tv"}); </script><script id="mps-page-integration">window.mpscall={ "cag[certified_fresh]": "0", "cag[fresh_rotten]": "rotten", "cag[genre]": "Comedy|Action", "cag[movieshow]": "Peacemaker", "cag[release]": "Aug 21, 2025", "cag[score]": "null", "cag[urlid]": "/peacemaker_2022/s02", "cat": "tv|season", "field[env]": "production", "field[rtid]": "76add34d-b98d-34b5-9e0f-2eac74d2ab10", "site": "rottentomatoes-web", "title": "Season 2", "type": "season"}; var mpsopts={ 'host': 'mps.nbcuni.com', 'updatecorrelator': 1}; var mps=mps ||{}; mps._ext=mps._ext ||{}; mps._adsheld=[]; mps._queue=mps._queue ||{}; mps._queue.mpsloaded=mps._queue.mpsloaded || []; mps._queue.mpsinit=mps._queue.mpsinit || []; mps._queue.gptloaded=mps._queue.gptloaded || []; mps._queue.adload=mps._queue.adload || []; mps._queue.adclone=mps._queue.adclone || []; mps._queue.adview=mps._queue.adview || []; mps._queue.refreshads=mps._queue.refreshads || []; mps.__timer=Date.now || function (){ return +new Date}; mps.__intcode="v2"; if (typeof mps.getAd !="function") mps.getAd=function (adunit){ if (typeof adunit !="string") return false; var slotid="mps-getad-" + adunit.replace(/\W/g, ""); if (!mps._ext || !mps._ext.loaded){ mps._queue.gptloaded.push(function (){ typeof mps._gptfirst=="function" && mps._gptfirst(adunit, slotid); mps.insertAd("#" + slotid, adunit)}); mps._adsheld.push(adunit)} return '<div id="' + slotid + '" class="mps-wrapper" data-mps-fill-slot="' + adunit + '"></div>'}; </script><script src="//mps.nbcuni.com/fetch/ext/load-rottentomatoes-web.js?nowrite=2" id="mps-load"></script><script type="application/ld+json">{"@context":"http://schema.org","@type":"TVSeason","actor":[{"@type":"Person","name":"John Cena","sameAs":"https://www.rottentomatoes.com/celebrity/john_cena","image":"https://resizing.flixster.com/qFr2ZK1qYDkqSmM5eT3nz_n6E_g=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/487578_v9_ba.jpg"},{"@type":"Person","name":"Danielle Brooks","sameAs":"https://www.rottentomatoes.com/celebrity/danielle_brooks","image":"https://resizing.flixster.com/KhnY5vsfjM0vtw0cZL3aNxXbeUE=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/765589_v9_bc.jpg"},{"@type":"Person","name":"Freddie Stroma","sameAs":"https://www.rottentomatoes.com/celebrity/freddie_stroma","image":"https://resizing.flixster.com/Yk2eiDCtamfmNlK-xMa7nmEw_Po=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/GNLZZGG00283ZZD.jpg"},{"@type":"Person","name":"Chukwudi Iwuji","sameAs":"https://www.rottentomatoes.com/celebrity/chukwudi_iwuji","image":"https://resizing.flixster.com/uNAFlG9dNMjJwyMbPDiCsbjkX8I=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/565157_v9_ba.jpg"},{"@type":"Person","name":"Jennifer Holland","sameAs":"https://www.rottentomatoes.com/celebrity/jennifer_holland","image":"https://resizing.flixster.com/-xeYAf0O7fGIQHRx_YkL7vnaMMg=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/331642_v9_bb.jpg"},{"@type":"Person","name":"Steve Agee","sameAs":"https://www.rottentomatoes.com/celebrity/steve_agee","image":"https://resizing.flixster.com/YprPSg0SXNIqq-Wy4UEz4ovBnOw=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/223358_v9_bd.jpg"}],"aggregateRating":{"@type":"AggregateRating","bestRating":"100","description":"The Tomatometer rating โ€“ based on the published opinions of hundreds of film and television critics โ€“ is a trusted measurement of movie and TV programming quality for millions of moviegoers. It represents the percentage of professional critic reviews that are positive for a given film or television show.","name":"Tomatometer","ratingCount":82,"ratingValue":"99","reviewCount":82,"worstRating":"0"},"dateCreated":"2025-08-21","description":"Discover reviews, ratings, and trailers for Peacemaker: Season 2 on Rotten Tomatoes. Stay updated with critic and audience scores today!","episode":[{"@type":"TVEpisode","episodeNumber":"1","url":"https://www.rottentomatoes.com/tv/peacemaker_2022/s02/e01","name":"The Ties That Grind","description":"While Peacemaker attempts to join the Justice Gang, Harcourt struggles to find work, and Economos takes on a challenging new assignment.","dateCreated":"2025-08-21"},{"@type":"TVEpisode","episodeNumber":"2","url":"https://www.rottentomatoes.com/tv/peacemaker_2022/s02/e02","name":"A Man Is Only as Good as His Bird","description":"As Economos clashes with his new handler, Peacemaker must deal with the consequences of his actions in the alternate dimension.","dateCreated":"2025-08-28"},{"@type":"TVEpisode","episodeNumber":"3","url":"https://www.rottentomatoes.com/tv/peacemaker_2022/s02/e03","name":"Another Rick Up My Sleeve","description":"A man fights for peace at any cost, no matter how many people he has to kill to get it.","dateCreated":"2025-09-04"},{"@type":"TVEpisode","episodeNumber":"4","url":"https://www.rottentomatoes.com/tv/peacemaker_2022/s02/e04","name":"Need I Say Door","description":"A man fights for peace at any cost, no matter how many people he has to kill to get it.","dateCreated":"2025-09-11"},{"@type":"TVEpisode","episodeNumber":"5","url":"https://www.rottentomatoes.com/tv/peacemaker_2022/s02/e05","name":"Back to the Suture","description":"A man fights for peace at any cost, no matter how many people he has to kill to get it.","dateCreated":"2025-09-18"},{"@type":"TVEpisode","episodeNumber":"6","url":"https://www.rottentomatoes.com/tv/peacemaker_2022/s02/e06","name":"Ignorance is Chris","description":"A man fights for peace at any cost, no matter how many people he has to kill to get it.","dateCreated":"2025-09-25"},{"@type":"TVEpisode","episodeNumber":"7","url":"https://www.rottentomatoes.com/tv/peacemaker_2022/s02/e07","description":"A man fights for peace at any cost, no matter how many people he has to kill to get it.","dateCreated":"2025-10-02"},{"@type":"TVEpisode","episodeNumber":"8","url":"https://www.rottentomatoes.com/tv/peacemaker_2022/s02/e08","description":"A man fights for peace at any cost, no matter how many people he has to kill to get it.","dateCreated":"2025-10-09"}],"genre":["Comedy","Action"],"image":"https://resizing.flixster.com/J-WXxwHSdv6w7VHxcCrPdebOvUA=/ems.cHJkLWVtcy1hc3NldHMvdHZzZWFzb24vNmE4MmUxOWItNGY5Yy00YzY0LTk5ODktZDY1MGEzZjdmZDFhLmpwZw==","name":"Season 2","partOfSeries":{"@type":"TVSeries","name":"Peacemaker","startDate":"2022-01-13","url":"https://www.rottentomatoes.com/tv/peacemaker_2022"},"producer":[{"@type":"Person","name":"James Gunn","sameAs":"https://www.rottentomatoes.com/celebrity/james_gunn","image":"https://images.fandango.com/cms/assets/b0cefeb0-b6a8-11ed-81d8-51a487a38835--poster-default-thumbnail.jpg"},{"@type":"Person","name":"Peter Safran","sameAs":"https://www.rottentomatoes.com/celebrity/peter_safran","image":"https://images.fandango.com/cms/assets/b0cefeb0-b6a8-11ed-81d8-51a487a38835--poster-default-thumbnail.jpg"}],"url":"https://www.rottentomatoes.com/tv/peacemaker_2022/s02","video":{"@type":"VideoObject","thumbnailUrl":"https://statcdn.fandango.com/MPX/image/NBCU_Fandango/166/403/thumb_47CFBBB8-95A3-4634-8B02-581F2C757D98.jpg","name":"Peacemaker: Season 2 Trailer - Weeks Ahead","duration":"1:39","sourceOrganization":"MPX","uploadDate":"2025-08-28T16:36:57","description":"","contentUrl":"https://www.rottentomatoes.com/tv/peacemaker_2022/s02/videos/nTePljVEct61"}}</script><link rel="manifest" href="https://www.rottentomatoes.com/assets/pizza-pie/manifest/manifest.json" /><link rel="apple-touch-icon" href="https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/apple-touch-icon-60.jpg" /><link rel="apple-touch-icon" sizes="152x152" href="https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/apple-touch-icon-152.jpg" /><link rel="apple-touch-icon" sizes="167x167" href="https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/apple-touch-icon-167.jpg" /><link rel="apple-touch-icon" sizes="180x180" href="https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/apple-touch-icon-180.jpg" /><meta name="apple-itunes-app" content="app-id=6673916573, app-argument=https://www.rottentomatoes.com/"><meta name="google-site-verification" content="VPPXtECgUUeuATBacnqnCm4ydGO99reF-xgNklSbNbc" /><meta name="msvalidate.01" content="034F16304017CA7DCF45D43850915323" /><meta name="theme-color" content="#FA320A"><meta http-equiv="x-dns-prefetch-control" content="on"><link rel="dns-prefetch" href="//www.rottentomatoes.com" /><link rel="preconnect" href="//www.rottentomatoes.com" /><link rel="stylesheet" href="/assets/pizza-pie/stylesheets/bundles/layouts/default.b6077682d41.css" /><link rel="preload" href="/assets/pizza-pie/stylesheets/bundles/pages/tvSeason.e90e54fbbb0.css" as="style" onload="this.onload=null;this.rel='stylesheet'" /><script>window.RottenTomatoes={}; window.RTLocals={}; window.nunjucksPrecompiled={}; window.__RT__={}; </script><script src="https://cdn.jwplayer.com/libraries/U8MHzHHR.js"></script><script src="https://sb.scorecardresearch.com/c2/plugins/streamingtag_plugin_jwplayer.js"></script><script>!function (e){ var n="https://s.go-mpulse.net/boomerang/"; if ("False"=="True") e.BOOMR_config=e.BOOMR_config ||{}, e.BOOMR_config.PageParams=e.BOOMR_config.PageParams ||{}, e.BOOMR_config.PageParams.pci=!0, n="https://s2.go-mpulse.net/boomerang/"; if (window.BOOMR_API_key="4RDDZ-2Z6GP-RRNMC-PYEUL-SK6K9", function (){ function e(){ if (!o){ var e=document.createElement("script"); e.id="boomr-scr-as", e.src=window.BOOMR.url, e.async=!0, i.parentNode.appendChild(e), o=!0}} function t(e){ o=!0; var n, t, a, r, d=document, O=window; if (window.BOOMR.snippetMethod=e ? "if" : "i", t=function (e, n){ var t=d.createElement("script"); t.id=n || "boomr-if-as", t.src=window.BOOMR.url, BOOMR_lstart=(new Date).getTime(), e=e || d.body, e.appendChild(t)}, !window.addEventListener && window.attachEvent && navigator.userAgent.match(/MSIE [67]\./)) return window.BOOMR.snippetMethod="s", void t(i.parentNode, "boomr-async"); a=document.createElement("IFRAME"), a.src="about:blank", a.title="", a.role="presentation", a.loading="eager", r=(a.frameElement || a).style, r.width=0, r.height=0, r.border=0, r.display="none", i.parentNode.appendChild(a); try{ O=a.contentWindow, d=O.document.open()} catch (_){ n=document.domain, a.src="javascript:var d=document.open();d.domain='" + n + "';void(0);", O=a.contentWindow, d=O.document.open()} if (n) d._boomrl=function (){ this.domain=n, t()}, d.write("<bo" + "dy onload='document._boomrl();'>"); else if (O._boomrl=function (){ t()}, O.addEventListener) O.addEventListener("load", O._boomrl, !1); else if (O.attachEvent) O.attachEvent("onload", O._boomrl); d.close()} function a(e){ window.BOOMR_onload=e && e.timeStamp || (new Date).getTime()} if (!window.BOOMR || !window.BOOMR.version && !window.BOOMR.snippetExecuted){ window.BOOMR=window.BOOMR ||{}, window.BOOMR.snippetStart=(new Date).getTime(), window.BOOMR.snippetExecuted=!0, window.BOOMR.snippetVersion=12, window.BOOMR.url=n + "4RDDZ-2Z6GP-RRNMC-PYEUL-SK6K9"; var i=document.currentScript || document.getElementsByTagName("script")[0], o=!1, r=document.createElement("link"); if (r.relList && "function"==typeof r.relList.supports && r.relList.supports("preload") && "as" in r) window.BOOMR.snippetMethod="p", r.href=window.BOOMR.url, r.rel="preload", r.as="script", r.addEventListener("load", e), r.addEventListener("error", function (){ t(!0)}), setTimeout(function (){ if (!o) t(!0)}, 3e3), BOOMR_lstart=(new Date).getTime(), i.parentNode.appendChild(r); else t(!1); if (window.addEventListener) window.addEventListener("load", a, !1); else if (window.attachEvent) window.attachEvent("onload", a)}}(), "".length >0) if (e && "performance" in e && e.performance && "function"==typeof e.performance.setResourceTimingBufferSize) e.performance.setResourceTimingBufferSize(); !function (){ if (BOOMR=e.BOOMR ||{}, BOOMR.plugins=BOOMR.plugins ||{}, !BOOMR.plugins.AK){ var n=""=="true" ? 1 : 0, t="", a="eyd6zaauaeceajqacqcoyaaaevul3mep-f-a8182a08c-clienttons-s.akamaihd.net", i="false"=="true" ? 2 : 1, o={ "ak.v": "39", "ak.cp": "1839344", "ak.ai": parseInt("1226367", 10), "ak.ol": "0", "ak.cr": 19, "ak.ipv": 6, "ak.proto": "h2", "ak.rid": "301a8e1c", "ak.r": 43885, "ak.a2": n, "ak.m": "dsca", "ak.n": "essl", "ak.bpcip": "2607:ec80:1401:440::", "ak.cport": 63193, "ak.gh": "23.199.45.20", "ak.quicv": "", "ak.tlsv": "tls1.3", "ak.0rtt": "", "ak.0rtt.ed": "", "ak.csrc": "-", "ak.acc": "", "ak.t": "1757261967", "ak.ak": "hOBiQwZUYzCg5VSAfCLimQ==ntUWb5wnRu6lt7Cbim0g4JNC0/ih9jlSOVTOOvHF5Ib49FleuDTLiBZtZs2c1+Y951ArMkVkvsjvdqe/amTFR0ODR3UKUgt+dr9I2Baxnr/2QXVqS168VfWbq3m5cyCJQAKuCLaoKSBsp5kGIj8vHcp61Ef1l5YlDWFrf/xl+gB+pnirvlyJ9s0bGxW2qDhKWeXYA9kUrRoVDldyiPVDUQquWWn234g1j+wMxq4wCdEVwBxK/haGHmMGN2mAm5X4rgUogJKHLQMvNQpuZaPTDoduN54Uy3piEtjFPaxEILb9jLhoJyhIsLum3/VAHrbucgxHEq8S+81KRp6Hyt8IeH0iZnic7or+2T/nFmRbXcoEaowMnKn3BC6TOUv2SIbK8INpCzzUvlhelrP40BEiYnfCcZLbcDg3OgKFdTlcZ0g=", "ak.pv": "3", "ak.dpoabenc": "", "ak.tf": i}; if ("" !==t) o["ak.ruds"]=t; var r={ i: !1, av: function (n){ var t="http.initiator"; if (n && (!n[t] || "spa_hard"===n[t])) o["ak.feo"]=void 0 !==e.aFeoApplied ? 1 : 0, BOOMR.addVar(o)}, rv: function (){ var e=["ak.bpcip", "ak.cport", "ak.cr", "ak.csrc", "ak.gh", "ak.ipv", "ak.m", "ak.n", "ak.ol", "ak.proto", "ak.quicv", "ak.tlsv", "ak.0rtt", "ak.0rtt.ed", "ak.r", "ak.acc", "ak.t", "ak.tf"]; BOOMR.removeVar(e)}}; BOOMR.plugins.AK={ akVars: o, akDNSPreFetchDomain: a, init: function (){ if (!r.i){ var e=BOOMR.subscribe; e("before_beacon", r.av, null, null), e("onbeacon", r.rv, null, null), r.i=!0} return this}, is_complete: function (){ return !0}}}}()}(window);</script></head><body class="body no-touch js-mptd-layout" data-AdsGlobalSkinTakeoverManager="body" data-SearchResultsNavManager="body"><cookie-manager></cookie-manager><device-inspection-manager endpoint="https://www.rottentomatoes.com/napi/device/inspection"></device-inspection-manager><user-activity-manager profiles-features-enabled="false"></user-activity-manager><user-identity-manager profiles-features-enabled="false"></user-identity-manager><ad-unit-manager></ad-unit-manager><auth-initiate-manager profiles-username-enabled="false" data-ArtiManager="authInitiateManager" data-WatchlistButtonManager="authInitiateManager:createAccount"></auth-initiate-manager><auth-profile-manager data-AuthInitiateManager="authProfileManager"></auth-profile-manager><auth-validation-manager data-AuthInitiateManager="authValidation"></auth-validation-manager><overlay-base data-AuthInitiateManager="overlayBase:close" data-PagePollsIndexManager="authOverlay:close" hidden><overlay-flows data-AuthInitiateManager="overlayFlows" slot="content"><action-icon slot="close" class="auth-overlay__icon-button auth-overlay__icon-button--close" aria-label="Close" data-qa="close-overlay-btn" icon="close"></action-icon></overlay-flows></overlay-base><notification-alert data-AuthInitiateManager="authSuccess" animate hidden><rt-icon icon="check-circled"></rt-icon><span>Signed in</span></notification-alert><div id="auth-templates" data-AuthInitiateManager="authTemplates"><template slot="screens" id="account-create-username-screen"><account-create-username-screen data-qa="account-create-username-screen"><input-label slot="input-username" state="default" data-qa="username-input-label"><label slot="label" for="create-username-input">Username</label><input slot="input" id="create-username-input" type="text" placeholder="Username" data-qa="username-input" /></input-label><rt-button disabled slot="btn-continue" shape="pill" data-qa="continue-btn">Continue</rt-button><rt-text class="terms-and-policies" slot="terms-and-policies" size="0.75">By joining, you agree to the <rt-link href="/policies/terms-and-policies" target="_blank" data-qa="terms-policies-link">Terms and Policies</rt-link>and the <rt-link href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" data-qa="privacy-policy-link">Privacy Policy</rt-link>and to receive email from <rt-link href="https://www.fandango.com/about-us" target="_blank" data-qa="about-fandango-link">Fandango Media Brands</rt-link>. </rt-text></account-create-username-screen></template><template slot="screens" id="account-email-change-screen"><account-email-change-screen data-qa="account-email-change-screen" email="user@email.com"><input-label class="new-email-input" state="default" slot="new-email-input" data-qa="email-input-label"><label slot="label" for="newEmail">Enter new email</label><input slot="input" name="newEmail" type="text" placeholder="Enter new email" autocomplete="off" data-qa="email-input"></input></input-label><rt-button slot="submit-button" disabled shape="pill" data-qa="submit-btn">Submit</rt-button><rt-text class="terms-and-policies" slot="terms-and-policies" size="0.75">By joining, you agree to the <rt-link href="/policies/terms-and-policies" target="_blank" data-qa="terms-policies-link">Terms and Policies</rt-link>and <rt-link href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" data-qa="privacy-policy-link">Privacy Policy</rt-link>and to receive email from the <rt-link href="https://www.fandango.com/about-us" target="_blank" data-qa="about-fandango-link">Fandango Media Brands</rt-link>. </rt-text></account-email-change-screen></template><template slot="screens" id="account-email-change-success-screen"><account-email-change-success-screen data-qa="login-create-success-screen"><rt-text slot="message" size="1.5" style="--fontWeight: var(--franklinGothicDemi);">Email change successful</rt-text><rt-text slot="submessage">You are signed out for your security. </br>Please sign in again.</rt-text><img slot="icon" src="/assets/pizza-pie/images/icons/cognito-success.3a3d5f32fab.svg" width="111" height="105" /></account-email-change-success-screen></template><template slot="screens" id="account-password-change-screen"><account-password-change-screen data-qa="account-password-change-screen"><input-label state="default" slot="input-password-existing"><label slot="label" for="password-existing">Existing password</label><input slot="input" name="password-existing" type="password" placeholder="Enter existing password" autocomplete="off"></input></input-label><input-label state="default" slot="input-password-new"><label slot="label" for="password-new">New password</label><input slot="input" name="password-new" type="password" placeholder="Enter new password" autocomplete="off"></input></input-label><rt-button disabled shape="pill" slot="submit-button">Submit</rt-button></account-password-change-screen></template><template slot="screens" id="account-password-change-updating-screen"><login-success-screen data-qa="account-password-change-updating-screen" hidebanner><rt-text slot="status" size="1.5" style="--fontWeight: var(--franklinGothicDemi);">Updating your password... </rt-text><img slot="icon" src="/assets/pizza-pie/images/icons/cognito-success.3a3d5f32fab.svg" width="111" height="105" /></login-success-screen></template><template slot="screens" id="account-password-change-success-screen"><login-success-screen data-qa="account-password-change-success-screen" hidebanner><rt-text slot="status" size="1.5" style="--fontWeight: var(--franklinGothicDemi);">Success! </rt-text><img slot="icon" src="/assets/pizza-pie/images/icons/cognito-success.3a3d5f32fab.svg" width="111" height="105" /></login-success-screen></template><template slot="screens" id="account-verifying-email-screen"><account-verifying-email-screen data-qa="account-verifying-email-screen"><img slot="icon" src="/assets/pizza-pie/images/icons/cognito-auth-verify.e74a69e9a77.svg" alt="email" /><rt-text slot="status">Verifying your email... </rt-text><rt-button type="cta-large" href="/?authFlowScreen=loginStartScreen" slot="retryLink" size="0.875" style="--fontWeight: var(--franklinGothicMedium);" data-qa="retry-link">Retry </rt-button></account-verifying-email-screen></template><template slot="screens" id="cognito-loading"><div><loading-spinner id="cognito-auth-loading-spinner"></loading-spinner><style>#cognito-auth-loading-spinner{ font-size: 2rem; transform: translate(calc(100% - 1em), 250px); width: 50%;} </style></div></template><template slot="screens" id="login-check-email-screen"><login-check-email-screen data-qa="login-check-email-screen" email="user@email.com"><rt-text class="note-text" size="1" slot="noteText">Please open the email link from the same browser you initiated the change email process from. </rt-text><rt-text slot="gotEmailMessage" size="0.875">Didn't you get the email? </rt-text><rt-button slot="resendEmailLink" size="0.875" type="cta-large" data-qa="resend-email-link">Resend email </rt-button><rt-link context="label" slot="troubleLoginLink" size="0.875" href="/reset-client" data-qa="reset-link">Having trouble logging in?</rt-link><rt-text class="terms-and-policies" slot="termsAndPolicies" size="0.75">By continuing, you agree to the <rt-link href="/policies/terms-and-policies" target="_blank" data-qa="terms-policies-link" style="--textColor: var(--blueLink);">Terms and Policies</rt-link>and the <rt-link href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" data-qa="privacy-policy-link" style="--textColor: var(--blueLink);">Privacy Policy</rt-link>and to receive email from <rt-link href="https://www.fandango.com/about-us" target="_blank" data-qa="about-fandango-link" style="--textColor: var(--blueLink);">Fandango Media Brands</rt-link>. </rt-text></login-check-email-screen></template><template slot="screens" id="login-error-screen"><login-error-screen data-qa="login-error"><rt-text slot="header" size="1.5" context="heading" data-qa="header">Something went wrong... </rt-text><rt-text slot="description1" size="1" context="label" data-qa="description1">Please try again. </rt-text><img slot="image" src="/assets/pizza-pie/images/icons/cognito-error.c55e509a7fd.svg" /><rt-text hidden slot="description2" size="1" context="label" data-qa="description2"></rt-text><rt-link slot="ctaLink" hidden context="label" size="0.875" data-qa="retry-link">Retry</rt-link></login-error-screen></template><template slot="screens" id="login-enter-password-screen"><login-enter-password-screen data-qa="login-enter-password-screen"><rt-text slot="title" size="1.5" style="--fontWeight: var(--franklinGothicMedium);">Welcome back! </rt-text><rt-text slot="username" data-qa="user-email">username@email.com </rt-text><input-label slot="inputPassword" state="default" data-qa="password-input-label"><label slot="label" for="pass">Password</label><input slot="input" id="pass" type="password" placeholder="Password" autocomplete="off" data-qa="password-input"></input></input-label><rt-button disabled slot="continueButton" type="cta-large" data-qa="continue-btn">Continue </rt-button><rt-button slot="emailLoginButton" theme="light" shape="pill" data-qa="send-email-btn">Send email to verify </rt-button><rt-link slot="forgotPasswordLink" theme="light" data-qa="forgot-password-link">Forgot password</rt-link><rt-text class="terms-and-policies" slot="termsAndPolicies" size="0.75">By continuing, you agree to the <rt-link href="/policies/terms-and-policies" target="_blank" data-qa="terms-policies-link" style="--textColor: var(--blueLink);">Terms and Policies</rt-link>and the <rt-link href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" data-qa="privacy-policy-link" style="--textColor: var(--blueLink);">Privacy Policy</rt-link>and to receive email from <rt-link href="https://www.fandango.com/about-us" target="_blank" data-qa="about-fandango-link" style="--textColor: var(--blueLink);">Fandango Media Brands</rt-link>. </rt-text></login-enter-password-screen></template><template slot="screens" id="login-start-screen"><login-start-screen data-qa="login-start-screen"><input-label slot="inputEmail" state="default" data-qa="email-input-label"><label slot="label" for="login-email-input">Email address</label><input slot="input" autocomplete="username" id="login-email-input" placeholder="Email address" type="text" data-qa="email-input" /></input-label><rt-button disabled slot="emailLoginButton" type="cta-large" data-qa="continue-btn">Continue </rt-button><rt-button slot="googleLoginButton" shape="pill" theme="light" style="--buttonHeight: 52px; --borderRadius: 32px;" data-qa="google-login-btn" data-type="google"><div class="social-login-btn-content"><img height="16px" width="16px" src="/assets/pizza-pie/images/vendor/google/google_logo.28d9eb28faa.svg" />Continue with Google </div></rt-button><rt-button slot="appleLoginButton" shape="pill" theme="light" style="--buttonHeight: 52px; --borderRadius: 32px;" data-qa="apple-login-btn" data-type="apple"><div class="social-login-btn-content"><rt-icon size="1" icon="apple"></rt-icon>Continue with apple </div></rt-button><rt-link slot="resetLink" class="reset-link" context="label" size="0.875" href="/reset-client" data-qa="reset-link">Having trouble logging in? </rt-link><rt-text class="terms-and-policies" slot="termsAndPolicies" size="0.75">By continuing, you agree to the <rt-link href="/policies/terms-and-policies" target="_blank" data-qa="terms-policies-link" style="--textColor: var(--blueLink);">Terms and Policies</rt-link>and the <rt-link href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" data-qa="privacy-policy-link" style="--textColor: var(--blueLink);">Privacy Policy</rt-link>and to receive email from <rt-link href="https://www.fandango.com/about-us" target="_blank" data-qa="about-fandango-link" style="--textColor: var(--blueLink);">Fandango Media Brands</rt-link>. </rt-text></login-start-screen></template><template slot="screens" id="login-success-screen"><login-success-screen data-qa="login-success-screen"><rt-text slot="status" size="1.5">Login successful! </rt-text><img slot="icon" src="/assets/pizza-pie/images/icons/cognito-success.3a3d5f32fab.svg" width="111" height="105" /></login-success-screen></template><template slot="screens" id="cognito-opt-in-us"><auth-optin-screen data-qa="auth-opt-in-screen"><div slot="newsletter-text"><h2 class="cognito-optin-form__header unset">Let's keep in touch!</h2></div><img slot="image" class="image" src="https://images.fandango.com/cms/assets/97c33f00-313f-11ee-9aaf-6762c75465cf--newsletter.png" alt="Rotten Tomatoes Newsletter"><h2 slot="sub-title" class="subTitle unset">Sign up for the Rotten Tomatoes newsletter to get weekly updates on: </h2><ul slot="options"><li class="icon-item">Upcoming Movies and TV shows</li><li class="icon-item">Rotten Tomatoes Podcast</li><li class="icon-item">Media News + More</li></ul><rt-button slot="opt-in-button" data-qa="auth-opt-in-screen-opt-in-btn">Sign me up </rt-button><rt-button slot="opt-out-button" class="button--outline" data-qa="auth-opt-in-screen-opt-out-btn">No thanks </rt-button><p slot="foot-note">By clicking "Sign Me Up," you are agreeing to receive occasional emails and communications from Fandango Media (Fandango, Vudu, and Rotten Tomatoes) and consenting to Fandango's <a href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" class="optin-link" target="_blank" rel="noopener" data-qa="auth-name-screen-privacy-policy-link">Privacy Policy</a>and <a href="/policies/terms-and-policies" class="optin-link" target="_blank" rel="noopener" data-qa="auth-name-screen-privacy-policy-link">Terms and Policies</a>. Please allow 10 business days for your account to reflect your preferences. </p></auth-optin-screen></template><template slot="screens" id="cognito-opt-in-foreign"><auth-optin-screen data-qa="auth-opt-in-screen"><div slot="newsletter-text"><h2 class="cognito-optin-form__header unset">Let's keep in touch!</h2></div><img slot="image" class="image" src="https://images.fandango.com/cms/assets/97c33f00-313f-11ee-9aaf-6762c75465cf--newsletter.png" alt="Rotten Tomatoes Newsletter"><h2 slot="sub-title" class="subTitle unset">Sign up for the Rotten Tomatoes newsletter to get weekly updates on: </h2><ul slot="options"><li class="icon-item">Upcoming Movies and TV shows</li><li class="icon-item">Rotten Tomatoes Podcast</li><li class="icon-item">Media News + More</li></ul><rt-button slot="opt-in-button" data-qa="auth-opt-in-screen-opt-in-btn">Sign me up </rt-button><rt-button slot="opt-out-button" class="button--outline" data-qa="auth-opt-in-screen-opt-out-btn">No thanks </rt-button></auth-optin-screen></template><template slot="screens" id="cognito-opt-in-success"><auth-verify-screen><rt-icon icon="check-circled" slot="icon"></rt-icon><p class="h3" slot="status">OK, got it!</p></auth-verify-screen></template></div><div id="emptyPlaceholder"></div><script ASYNC src="//assets.adobedtm.com/launch-EN549327edc13e414a9beb5d61bfd9aac6.min.js"></script><div id="main" class="container rt-layout__body"><a href="#main-page-content" class="skip-link">Skip to Main Content</a><div id="header_and_leaderboard"><div id="top_leaderboard_wrapper" class="leaderboard_wrapper "><ad-unit hidden unit-display="desktop" unit-type="topbanner" adjust-height><div slot="ad-inject"></div></ad-unit><ad-unit hidden unit-display="mobile" unit-type="mbanner"><div slot="ad-inject"></div></ad-unit></div></div><rt-header-manager></rt-header-manager><rt-header aria-label="navigation bar" class="navbar" data-qa="header-nav-bar" data-AdsGlobalNavTakeoverManager="header" id="header-main" skeleton="panel"><button aria-label="Open aRTi" class="arti-mobile" data-ArtiManager="btnArti:click" slot="arti-mobile"><img alt="arti" src="/assets/pizza-pie/images/arti.041d204c4a4.svg" /></button><div slot="mobile-header-nav"><rt-button id="mobile-header-nav-btn" data-RtHeaderManager="mobileHeaderNavBtn:click" size="1.6" style="--backgroundColor: transparent; --backgroundColorHover: transparent; --buttonPadding: 0 10px 4px;">&#9776; </rt-button><mobile-header-nav id="mobile-header-nav" data-RtHeaderManager="mobileHeaderNav"><rt-img slot="logoImage" alt="Rotten Tomatoes" fetchpriority="high" src="/assets/pizza-pie/images/rt-tomato-logo.20c3bdbc97b.svg"></rt-img><div slot="menusCss"></div><div slot="menus"></div></mobile-header-nav></div><a class="logo-wrap" data-AdsGlobalNavTakeoverManager="logoLink" data-SearchResultsNavManager="rtNavLogo" href="/" id="navbar" slot="logo"><img alt="Rotten Tomatoes" data-qa="header-logo" data-AdsGlobalNavTakeoverManager="logo" src="/assets/pizza-pie/images/rtlogo.9b892cff3fd.png" fetchpriority="high" /><div class="hide"><ad-unit hidden unit-display="desktop,mobile" unit-type="logorepeat" unit-targeting="ploc=rtlogo;"><div slot="ad-inject"></div></ad-unit></div></a><search-results-nav-manager></search-results-nav-manager><search-results-nav data-adobe-id="global-nav-search" data-SearchResultsNavManager="search" slot="search" skeleton="chip"><search-results-controls data-SearchResultsNavManager="searchControls" slot="controls"><input aria-label="Search" data-AdsGlobalNavTakeoverManager="searchInput" data-SearchResultsNavManager="inputText:click,input,keydown" data-qa="search-input" placeholder="Search" slot="search-input" type="text" /><rt-button class="search-clear" data-qa="search-clear" data-AdsGlobalNavTakeoverManager="searchClearBtn" data-SearchResultsNavManager="clearBtn:click" size="0.875" slot="search-clear" theme="transparent"><rt-icon icon="close"></rt-icon></rt-button><rt-link class="search-submit" aria-label="Submit search" data-qa="search-submit" data-AdsGlobalNavTakeoverManager="searchSubmitBtn" data-SearchResultsNavManager="submitBtn:click" href="/search" size="0.875" slot="search-submit"><rt-icon icon="search"></rt-icon></rt-link><rt-button class="search-cancel" data-qa="search-cancel" data-AdsGlobalNavTakeoverManager="searchCancelBtn" data-SearchResultsNavManager="cancelBtn:click" size="0.875" slot="search-cancel" theme="transparent">Cancel </rt-button></search-results-controls><search-results aria-expanded="false" class="hide" data-SearchResultsNavManager="searchResults" slot="results"></search-results></search-results-nav><ul slot="nav-links"><li><a href="/about" data-qa="header:link-whats-tmeter" data-AdsGlobalNavTakeoverManager="text">About Rotten Tomatoes&reg; </a></li><li><a href="/critics" data-qa="header:link-critics-home" data-AdsGlobalNavTakeoverManager="text">Critics </a></li><li data-RtHeaderManager="loginLink"><ul><li><button id="masthead-show-login-btn" class="js-cognito-signin button--link" data-AuthInitiateManager="btnSignIn:click" data-qa="header:login-btn" data-AdsGlobalNavTakeoverManager="text">Login/signup </button></li></ul></li><li class="hide" data-RtHeaderManager="userItem:keydown,keyup,mouseenter" data-qa="header:user"><a class="masthead-user-link" data-RtHeaderManager="navUserlink:focus" rel="nofollow" data-qa="user-profile-link"><img data-RtHeaderManager="navUserImg" data-qa="user-profile-thumb"><p data-AdsGlobalNavTakeoverManager="text" data-RtHeaderManager="navUserFirstName" data-qa="user-profile-name"></p><rt-icon data-AdsGlobalNavTakeoverManager="text" icon="down-dir" image></rt-icon></a><rt-header-user-info class="hide" data-RtHeaderManager="userInfo:focusout,mouseleave"><a data-qa="user-stats-profile-pic" href="" rel="nofollow" slot="imageExpanded" tabindex="-1"><img src="" width="40" alt=""></a><a slot="fullName" rel="nofollow" href="" class="username" data-qa="user-stats-name"></a><a slot="wts" rel="nofollow" href="" class="wts-count-block" data-qa="user-stats-wts"><rt-icon icon="plus" data-qa="user-stats-ratings-count"></rt-icon><span class="count" data-qa="user-stats-wts-count"></span>&nbsp;Wants to See </a><a slot="rating" rel="nofollow" href="" class="rating-count-block" data-qa="user-stats-ratings"><rt-icon icon="star" data-qa="user-stats-ratings-count"></rt-icon><span class="count"></span>&nbsp;Ratings </a><a slot="profileLink" rel="nofollow" class="dropdown-link" href="" data-qa="user-stats-profile-link">Profile</a><a slot="accountLink" rel="nofollow" class="dropdown-link" href="/user/account" data-qa="user-stats-account-link">Account</a><a slot="logoutLink" class="dropdown-link" data-RtHeaderManager="logoutLink:click" href="#logout" data-qa="user-stats-logout-link">Log Out</a></rt-header-user-info></li></ul><rt-header-nav slot="nav-dropdowns"><button aria-label="Open aRTi" class="arti-desktop" data-ArtiManager="btnArti:click" slot="arti-desktop"><img alt="arti" src="/assets/pizza-pie/images/arti.041d204c4a4.svg" /></button><rt-header-nav-item slot="movies" data-qa="masthead:movies-dvds"><a class="unset" slot="link" href="/browse/movies_in_theaters/sort:popular" data-qa="masthead:movies-dvds-link" data-AdsGlobalNavTakeoverManager="text">Movies </a><rt-header-nav-item-dropdown aria-expanded="false" slot="dropdown" data-qa="movies-menu"><rt-header-nav-item-dropdown-list slot="column" data-qa="header-movies-in-theaters"><p slot="title" class="h4" data-qa="movies-in-theaters-main-link"><a class="unset" href="/browse/movies_in_theaters/sort:popular">Movies in theaters</a></p><ul slot="links"><li data-qa="in-theaters-item"><a href="/browse/movies_in_theaters/sort:newest" data-qa="opening-this-week-link">Opening This Week</a></li><li data-qa="in-theaters-item"><a href="/browse/movies_in_theaters/sort:top_box_office" data-qa="top-box-office-link">Top Box Office</a></li><li data-qa="in-theaters-item"><a href="/browse/movies_coming_soon/" data-qa="coming-soon-link">Coming Soon to Theaters</a></li><li data-qa="in-theaters-item"><a href="/browse/movies_in_theaters/critics:certified_fresh~sort:popular" data-qa="certified-fresh-link">Certified Fresh Movies</a></li></ul></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-on-dvd-streaming"><p slot="title" class="h4" data-qa="dvd-streaming-main-link"><a class="unset" href="/browse/movies_at_home">Movies at Home</a></p><ul slot="links"><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/affiliates:fandango-at-home" data-qa="fandango-at-home-link">Fandango at Home</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/affiliates:peacock" data-qa="peacock-link">Peacock</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/affiliates:netflix" data-qa="netflix-link">Netflix</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/affiliates:apple-tv-plus" data-qa="apple-tv-link">Apple TV+</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/affiliates:prime-video" data-qa="prime-video-link">Prime Video</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/sort:popular" data-qa="most-popular-streaming-movies-link">Most Popular Streaming movies</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home/critics:certified_fresh" data-qa="certified-fresh-movies-link">Certified Fresh movies</a></li><li data-qa="movies-at-home-item"><a href="/browse/movies_at_home" data-qa="browse-all-link">Browse all</a></li></ul></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-movies-more"><p slot="title" class="h4">More</p><ul slot="links"><li data-qa="what-to-watch-item"><a href="https://editorial.rottentomatoes.com/rt-hub/what-to-watch" class="what-to-watch" data-qa="what-to-watch-link">What to Watch<rt-badge>New</rt-badge></a></li></ul></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" cfp><p slot="title" class="h4">Certified fresh picks</p><ul slot="links" class="cfp-wrap" data-qa="header-certified-fresh-picks" data-curation="rt-nav-list-cf-picks"><li data-qa="cert-fresh-item"><a class="cfp-tile" href="/m/twinless" data-qa="cert-fresh-link"><tile-dynamic data-qa="tile"><rt-img alt="Twinless poster image" slot="image" src="https://resizing.flixster.com/j7lw2KeY9_XyfZQdqRZGku7_9C8=/206x305/v2/https://resizing.flixster.com/uxoeWz7uWmeYIV94_SzEV_osqe4=/fit-in/180x240/v2/https://resizing.flixster.com/VlylB3xT2RIYmRivMx37O3yD76Q=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2ZlNDQ1MGQ5LTFjN2QtNDIwNC04NWE1LTM5NGM4N2U5ZTgzYy5qcGc=" loading="lazy"></rt-img><div slot="caption" data-track="scores"><div class="score-wrap"><score-icon-critics certified sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" size="1" context="label">98%</rt-text></div><span class="p--small">Twinless</span><span class="sr-only">Link to Twinless</span></div></tile-dynamic></a></li><li data-qa="cert-fresh-item"><a class="cfp-tile" href="/m/hamilton_2020" data-qa="cert-fresh-link"><tile-dynamic data-qa="tile"><rt-img alt="Hamilton poster image" slot="image" src="https://resizing.flixster.com/1woquJmQfEhWCZtm7GcH0NMHsYA=/206x305/v2/https://resizing.flixster.com/PeAJ5ZpF5qB98ZiX6ixNDCgW2P0=/fit-in/180x240/v2/https://resizing.flixster.com/VmBvlTk8-z7pQvDZXTgSdj93WDE=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzkzY2IxZjFkLTE1NjEtNDQ4Yi05NDY3LTcxNzFmMDVhMDczNi5qcGc=" loading="lazy"></rt-img><div slot="caption" data-track="scores"><div class="score-wrap"><score-icon-critics certified sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" size="1" context="label">98%</rt-text></div><span class="p--small">Hamilton</span><span class="sr-only">Link to Hamilton</span></div></tile-dynamic></a></li><li data-qa="cert-fresh-item"><a class="cfp-tile" href="/m/the_thursday_murder_club" data-qa="cert-fresh-link"><tile-dynamic data-qa="tile"><rt-img alt="The Thursday Murder Club poster image" slot="image" src="https://resizing.flixster.com/jeeldFGcfSMgG09ey5VB7TCFiek=/206x305/v2/https://resizing.flixster.com/9LXDkCzIBBNEiPURkB9t6VefF5Q=/fit-in/180x240/v2/https://resizing.flixster.com/rwdeR5xIiN0k7SWr6yXdnmb6zP8=/ems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2EzYWFkZWJiLWE5N2MtNDc3MS1iMDRlLTk0YWVlYzI5M2UxZS5qcGc=" loading="lazy"></rt-img><div slot="caption" data-track="scores"><div class="score-wrap"><score-icon-critics certified sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" size="1" context="label">76%</rt-text></div><span class="p--small">The Thursday Murder Club</span><span class="sr-only">Link to The Thursday Murder Club</span></div></tile-dynamic></a></li></ul></rt-header-nav-item-dropdown-list></rt-header-nav-item-dropdown></rt-header-nav-item><rt-header-nav-item slot="tv" data-qa="masthead:tv"><a class="unset" slot="link" href="/browse/tv_series_browse/sort:popular" data-qa="masthead:tv-link" data-AdsGlobalNavTakeoverManager="text">Tv shows </a><rt-header-nav-item-dropdown aria-expanded="false" slot="dropdown" data-qa="tv-menu"><rt-header-nav-item-dropdown-list slot="column" data-qa="header-tv-list1"><p slot="title" class="h4" data-curation="rt-hp-text-list-3">New TV Tonight </p><ul slot="links" class="score-list-wrap"><li data-qa="list-item"><a class="score-list-item" href="/tv/task/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">89%</rt-text></div><span>Task: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/the_walking_dead_daryl_dixon/s03" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">80%</rt-text></div><span>The Walking Dead: Daryl Dixon: Season 3 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/the_crow_girl/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">80%</rt-text></div><span>The Crow Girl: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/only_murders_in_the_building/s05" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>Only Murders in the Building: Season 5 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/the_girlfriend/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>The Girlfriend: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/aka_charlie_sheen/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>aka Charlie Sheen: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/wizards_beyond_waverly_place/s02" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>Wizards Beyond Waverly Place: Season 2 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/seen_and_heard_the_history_of_black_television/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>Seen &amp; Heard: the History of Black Television: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/the_fragrant_flower_blooms_with_dignity/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>The Fragrant Flower Blooms With Dignity: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/guts_and_glory/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="empty" size="1"></score-icon-critics><rt-text class="critics-score-empty" context="label" size="1" style="--textColor: var(--grayLight4); --lineHeight: 1; --letterSpacing: 0.2em;">--</rt-text></div><span>Guts &amp; Glory: Season 1 </span></a></li></ul><a class="a--short" data-qa="tv-list1-view-all-link" href="/browse/tv_series_browse/sort:newest" slot="view-all-link">View All </a></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-tv-list2"><p slot="title" class="h4" data-curation="rt-hp-text-list-2">Most Popular TV on RT </p><ul slot="links" class="score-list-wrap"><li data-qa="list-item"><a class="score-list-item" href="/tv/the_paper_2025/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">83%</rt-text></div><span>The Paper: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/dexter_resurrection/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">95%</rt-text></div><span>Dexter: Resurrection: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/alien_earth/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">95%</rt-text></div><span>Alien: Earth: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/task/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">89%</rt-text></div><span>Task: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/wednesday/s02" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">87%</rt-text></div><span>Wednesday: Season 2 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/peacemaker_2022/s02" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">99%</rt-text></div><span>Peacemaker: Season 2 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/the_terminal_list_dark_wolf/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">73%</rt-text></div><span>The Terminal List: Dark Wolf: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/hostage_2025/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">82%</rt-text></div><span>Hostage: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/chief_of_war/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="true" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">93%</rt-text></div><span>Chief of War: Season 1 </span></a></li><li data-qa="list-item"><a class="score-list-item" href="/tv/irish_blood/s01" data-qa="list-item-link"><div class="score-wrap"><score-icon-critics certified="false" sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" context="label" size="1" style="--lineHeight: 1; --letterSpacing: 0.016em;">100%</rt-text></div><span>Irish Blood: Season 1 </span></a></li></ul><a class="a--short" data-qa="tv-list2-view-all-link" href="/browse/tv_series_browse/sort:popular?" slot="view-all-link">View All </a></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-tv-more"><p slot="title" class="h4">More</p><ul slot="links"><li><a href="https://editorial.rottentomatoes.com/rt-hub/what-to-watch/" class="what-to-watch" data-qa="what-to-watch-link-tv">What to Watch<rt-badge>New</rt-badge></a></li><li><a href="/browse/tv_series_browse/sort:popular" data-qa="tv-best-link"><span>Best TV Shows</span></a></li><li><a href="/browse/tv_series_browse/sort:popular" data-qa="tv-popular-link"><span>Most Popular TV</span></a></li><li><a href="/browse/tv_series_browse/affiliates:fandango-at-home" data-qa="tv-fandango-at-home-link"><span>Fandango at Home</span></a></li><li><a href="/browse/tv_series_browse/affiliates:peacock" data-qa="tv-peacock-link"><span>Peacock</span></a></li><li><a href="/browse/tv_series_browse/affiliates:paramount-plus" data-qa="tv-paramount-link"><span>Paramount+</span></a></li><li><a href="/browse/tv_series_browse/affiliates:netflix" data-qa="tv-netflix-link"><span>Netflix</span></a></li><li><a href="/browse/tv_series_browse/affiliates:prime-video" data-qa="tv-prime-video-link"><span>Prime Video</span></a></li><li><a href="/browse/tv_series_browse/affiliates:apple-tv-plus" data-qa="tv-apple-tv-plus-link"><span>Apple TV+</span></a></li></ul></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" cfp data-qa="header-certified-fresh-pick"><p slot="title" class="h4">Certified fresh pick </p><ul slot="links" class="cfp-wrap" data-curation="rt-nav-list-cf-picks"><li><a class="cfp-tile" href="/tv/the_paper_2025/s01" data-qa="cert-fresh-link"><tile-dynamic data-qa="tile"><rt-img alt="The Paper: Season 1 poster image" slot="image" src="https://resizing.flixster.com/yFijQcjPYUWUelgmiZLHgkXU7hw=/206x305/v2/https://resizing.flixster.com/DFkkHf5pEVX_apKtIQZcoEvI6RU=/fit-in/180x240/v2/https://resizing.flixster.com/texEZJLAG-KcVpfCdkT2R1t4cmE=/ems.cHJkLWVtcy1hc3NldHMvdHZzZWFzb24vYTM3OWM2MTctN2M3Ny00MjdhLTk4NDUtODE5ZWUwMWExNGRhLnBuZw==" loading="lazy"></rt-img><div slot="caption" data-track="scores"><div class="score-wrap"><score-icon-critics certified sentiment="positive" size="1"></score-icon-critics><rt-text class="critics-score" size="1" context="label">83%</rt-text></div><span class="p--small">The Paper: Season 1</span><span class="sr-only">Link to The Paper: Season 1</span></div></tile-dynamic></a></li></ul></rt-header-nav-item-dropdown-list></rt-header-nav-item-dropdown></rt-header-nav-item><rt-header-nav-item slot="shop"><a class="unset" id="appLink" slot="link" href="https://editorial.rottentomatoes.com/article/app/" target="_blank" data-qa="masthead:app-link" data-AdsGlobalNavTakeoverManager="text">RT App <temporary-display slot="temporary-display" key="app" element="#appLink" event="click"><rt-badge hidden>New</rt-badge></temporary-display></a></rt-header-nav-item><rt-header-nav-item slot="news" data-qa="masthead:news"><a class="unset" slot="link" href="https://editorial.rottentomatoes.com/" data-qa="masthead:news-link" data-AdsGlobalNavTakeoverManager="text">News </a><rt-header-nav-item-dropdown aria-expanded="false" slot="dropdown" data-qa="news-menu"><rt-header-nav-item-dropdown-list slot="column" data-qa="header-news-columns"><p slot="title" class="h4">Columns</p><ul slot="links"><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/all-time-lists/" data-pageheader="All-Time Lists" data-qa="column-link">All-Time Lists </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/binge-guide/" data-pageheader="Binge Guide" data-qa="column-link">Binge Guide </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/comics-on-tv/" data-pageheader="Comics on TV" data-qa="column-link">Comics on TV </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/countdown/" data-pageheader="Countdown" data-qa="column-link">Countdown </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/five-favorite-films/" data-pageheader="Five Favorite Films" data-qa="column-link">Five Favorite Films </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/video-interviews/" data-pageheader="Video Interviews" data-qa="column-link">Video Interviews </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/weekend-box-office/" data-pageheader="Weekend Box Office" data-qa="column-link">Weekend Box Office </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/weekly-ketchup/" data-pageheader="Weekly Ketchup" data-qa="column-link">Weekly Ketchup </a></li><li data-qa="column-item"><a href="https://editorial.rottentomatoes.com/what-to-watch/" data-pageheader="What to Watch" data-qa="column-link">What to Watch </a></li></ul></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-news-guides"><p slot="title" class="h4">Guides</p><ul slot="links" class="news-wrap"><li data-qa="guides-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/guide/best-football-movies/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="59 Best Football Movies, Ranked by Tomatometer poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2023/09/600EssentialFootballMovies.png" loading="lazy"></rt-img><div slot="caption"><p>59 Best Football Movies, Ranked by Tomatometer</p><span class="sr-only">Link to 59 Best Football Movies, Ranked by Tomatometer</span></div></tile-dynamic></a></li><li data-qa="guides-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/guide/best-new-rom-coms-romance-movies/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="50 Best New Rom-Coms and Romance Movies poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2025/08/Best_New_Romcoms600.jpg" loading="lazy"></rt-img><div slot="caption"><p>50 Best New Rom-Coms and Romance Movies</p><span class="sr-only">Link to 50 Best New Rom-Coms and Romance Movies</span></div></tile-dynamic></a></li></ul><a class="a--short" data-qa="guides-view-all-link" href="https://editorial.rottentomatoes.com/countdown/" slot="view-all-link">View All </a></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-news-hubs"><p slot="title" class="h4">Hubs</p><ul slot="links" class="news-wrap"><li data-qa="hubs-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/rt-hub/what-to-watch/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="What to Watch: In Theaters and On Streaming poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2023/05/RT_WTW_Generic_2023_Thumbnail_600x314_021623.jpg" loading="lazy"></rt-img><div slot="caption"><p>What to Watch: In Theaters and On Streaming</p><span class="sr-only">Link to What to Watch: In Theaters and On Streaming</span></div></tile-dynamic></a></li><li data-qa="hubs-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/rt-hub/awards-tour/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="Awards Tour poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2023/02/RT_AwardsTour_Thumbnail_600x314.jpg" loading="lazy"></rt-img><div slot="caption"><p>Awards Tour</p><span class="sr-only">Link to Awards Tour</span></div></tile-dynamic></a></li></ul><a class="a--short" data-qa="hubs-view-all-link" href="https://editorial.rottentomatoes.com/rt-hubs/" slot="view-all-link">View All </a></rt-header-nav-item-dropdown-list><rt-header-nav-item-dropdown-list slot="column" data-qa="header-news-rt-news"><p slot="title" class="h4">RT News</p><ul slot="links" class="news-wrap"><li data-qa="rt-news-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/article/new-movies-and-shows-streaming-in-september-2025-what-to-watch-on-netflix-prime-video-hbo-max-disney-and-more/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="New Movies and Shows Streaming in September: What to watch on Netflix, Prime Video, HBO Max, Disney+ and More poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2025/08/New_Streaming_September_2025-Rep.jpg" loading="lazy"></rt-img><div slot="caption"><p>New Movies and Shows Streaming in September: What to watch on Netflix, Prime Video, HBO Max, Disney+ and More</p><span class="sr-only">Link to New Movies and Shows Streaming in September: What to watch on Netflix, Prime Video, HBO Max, Disney+ and More</span></div></tile-dynamic></a></li><li data-qa="rt-news-item"><a class="news-tile" href="https://editorial.rottentomatoes.com/article/the-conjuring-last-rites-first-reviews/" data-qa="news-link"><tile-dynamic data-qa="tile" orientation="landscape"><rt-img alt="<em>The Conjuring: Last Rites</em>First Reviews: A Frightful, Fitting Send-off poster image" slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2025/09/Conjuring_Last_Rites_Reviews-Rep.jpg" loading="lazy"></rt-img><div slot="caption"><p><em>The Conjuring: Last Rites</em>First Reviews: A Frightful, Fitting Send-off</p><span class="sr-only">Link to <em>The Conjuring: Last Rites</em>First Reviews: A Frightful, Fitting Send-off</span></div></tile-dynamic></a></li></ul><a class="a--short" data-qa="rt-news-view-all-link" href="https://editorial.rottentomatoes.com/news/" slot="view-all-link">View All </a></rt-header-nav-item-dropdown-list></rt-header-nav-item-dropdown></rt-header-nav-item><rt-header-nav-item slot="showtimes"><a class="unset" slot="link" href="https://www.fandango.com/movies-in-theaters?a=13036" target="_blank" rel="noopener" data-qa="masthead:tickets-showtimes-link" data-AdsGlobalNavTakeoverManager="text">Showtimes </a></rt-header-nav-item></rt-header-nav></rt-header><ads-global-nav-takeover-manager></ads-global-nav-takeover-manager><section class="trending-bar"><ad-unit hidden id="trending_bar_ad" unit-display="desktop" unit-type="trendinggraphic"><div slot="ad-inject"></div></ad-unit><div id="trending-bar-start" class="trending-list-wrap" data-qa="trending-bar"><ul class="list-inline trending-bar__list" data-curation="rt-nav-trending" data-qa="trending-bar-list"><li class="trending-bar__header">Trending on RT</li><li><a class="trending-bar__link" href="https://editorial.rottentomatoes.com/article/2025-emmys-ballot-complete-with-tomatometer-and-popcornmeter-scores/" data-qa="trending-bar-item">Emmy Noms </a></li><li><a class="trending-bar__link" href="https://editorial.rottentomatoes.com/article/movie-re-releases-calendar/" data-qa="trending-bar-item">Re-Release Calendar </a></li><li><a class="trending-bar__link" href="https://editorial.rottentomatoes.com/article/renewed-and-cancelled-tv-shows-2025/" data-qa="trending-bar-item">Renewed and Cancelled TV </a></li><li><a class="trending-bar__link" href="https://editorial.rottentomatoes.com/article/app/" data-qa="trending-bar-item">The Rotten Tomatoes App </a></li></ul><div class="trending-bar__social" data-qa="trending-bar-social-list"><social-media-icons theme="light" size="14"></social-media-icons></div></div></section><main id="main_container" class="container rt-layout__content"><div id="main-page-content"><div id="tv-season-overview" data-HeroModulesManager="overviewWrap"><watchlist-button-manager></watchlist-button-manager><div id="hero-wrap" data-AdUnitManager="heroWrap" data-AdsMediaScorecardManager="heroWrap" data-HeroModulesManager="heroWrap"><nav class="tv-navigation" skeleton="box" data-TvNavigationManager="tvNavigation" data-HeroModulesManager="tvNavigation"><rt-link data-TvNavigationManager="prevLink" href="/tv/peacemaker_2022" size="0.875"><rt-icon icon="left-chevron"></rt-icon>Series Page </rt-link><tv-navigation-manager><script data-json="tvNavigation" type="application/json">{"returnLink":{"href":"/tv/peacemaker_2022","text":"Series Page"},"seasons":[{"href":"/tv/peacemaker_2022/s01","isSelected":false,"text":"Season 1","value":"Season 1"},{"href":"/tv/peacemaker_2022/s02","isSelected":true,"text":"Season 2","value":"Season 2"}]}</script></tv-navigation-manager><div class="dropdown-actions"><div class="seasons-wrap"><rt-button data-TvNavigationManager="seasonsBtn:click,keydown" size="0.8755" skeleton="panel" theme="simplified">Season -- </rt-button><dropdown-menu data-HeroModulesManager="tvNavMenu:close,open" data-TvNavigationManager="seasonsMenu:close" name="seasonsMenu" hidden><rt-link href="/tv/peacemaker_2022/s01" isselected="false" size="1" slot="option" context="label"><dropdown-option ellipsis value="Season 1">Season 1 </dropdown-option></rt-link><rt-link href="/tv/peacemaker_2022/s02" isselected="true" size="1" slot="option" context="label"><dropdown-option ellipsis value="Season 2">Season 2 </dropdown-option></rt-link></dropdown-menu></div><div class="episodes-wrap"><rt-button data-TvNavigationManager="episodesBtn:click,keydown" size="0.8755" skeleton="panel" theme="simplified">Episode -- </rt-button><dropdown-menu data-HeroModulesManager="tvNavMenu:close,open" data-TvNavigationManager="episodesMenu:close" name="episodesMenu" hidden></dropdown-menu></div></div></nav><div aria-labelledby="media-hero-label" class="media-hero-wrap" skeleton="panel" data-adobe-id="media-hero" data-qa="section:media-hero" data-HeroModulesManager="mediaHeroWrap"><h1 class="unset" id="media-hero-label"><sr-text>Season 2 &ndash; Peacemaker </sr-text></h1><media-hero averagecolor="33,54,15" mediatype="TvSeason" scrolly="0" scrollystart="0" data-AdsMediaScorecardManager="mediaHero" data-HeroModulesManager="mediaHero:collapse"><rt-button slot="iconicVideoCta" theme="transparent" data-content-type="PROMO" data-ems-id="76add34d-b98d-34b5-9e0f-2eac74d2ab10" data-mpx-id="2447231043621" data-public-id="nTePljVEct61" data-title="Season 2 &ndash; Peacemaker" data-type="TvSeason" data-VideoPlayerOverlayManager="btnVideo:click"><sr-text>Play trailer</sr-text></rt-button><rt-text slot="iconicVideoRuntime" size="0.75">1:39</rt-text><rt-img slot="iconic" alt="Main image for Season 2 &amp;ndash; Peacemaker" fallbacktheme="iconic" fetchpriority="high" src="https://resizing.flixster.com/bvZfzyIQHc8UI0CJS9UdOdRXC7w=/375x210/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/166/403/thumb_47CFBBB8-95A3-4634-8B02-581F2C757D98.jpg,https://resizing.flixster.com/2jGm07y7TAmgwksV1KSg9Xsogtg=/620x336/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/166/403/thumb_47CFBBB8-95A3-4634-8B02-581F2C757D98.jpg"></rt-img><img slot="poster" alt="Poster for " fetchpriority="high" src="https://resizing.flixster.com/kNXWRTdmL5I0O6L6XKMfIje7Qaw=/68x102/v2/https://resizing.flixster.com/J-WXxwHSdv6w7VHxcCrPdebOvUA=/ems.cHJkLWVtcy1hc3NldHMvdHZzZWFzb24vNmE4MmUxOWItNGY5Yy00YzY0LTk5ODktZDY1MGEzZjdmZDFhLmpwZw==" /><rt-text slot="title" size="1.25,1.75" context="heading">Season 2 &ndash; Peacemaker</rt-text><rt-text slot="episodeTitle" size="1,1.5" context="label"></rt-text><rt-text slot="metadataProp" context="label" size="0.875">Next Ep Thu Sep 11</rt-text><rt-text slot="metadataGenre" size="0.875">Comedy</rt-text><rt-text slot="metadataGenre" size="0.875">Action</rt-text><rt-button slot="trailerCta" shape="pill" theme="light" data-content-type="PROMO" data-ems-id="76add34d-b98d-34b5-9e0f-2eac74d2ab10" data-mpx-id="2447231043621" data-public-id="nTePljVEct61" data-title="Season 2 &ndash; Peacemaker" data-type="TvSeason" data-VideoPlayerOverlayManager="btnVideo:click"><rt-icon icon="play"></rt-icon><sr-text>Play</sr-text>Trailer </rt-button><watchlist-button slot="watchlistCta" emsid="76add34d-b98d-34b5-9e0f-2eac74d2ab10" mediatype="TvSeason" mediatitle="Season 2 &ndash; Peacemaker" state="unchecked" theme="transparent-lighttext" data-HeroModulesManager="mediaHeroWatchlistBtn" data-WatchlistButtonManager="watchlistButton:click"><span slot="text">Watchlist</span></watchlist-button><watchlist-button slot="mobileWatchlistCta" emsid="76add34d-b98d-34b5-9e0f-2eac74d2ab10" mediatype="TvSeason" mediatitle="Season 2 &ndash; Peacemaker" state="unchecked" data-HeroModulesManager="mediaHeroWatchlistBtn" data-WatchlistButtonManager="watchlistButton:click"></watchlist-button><div slot="desktopVideos" data-HeroModulesManager="mediaHeroVideos"></div><rt-button slot="collapsedPrimaryCta" hidden shape="pill" theme="simplified" data-AdsMediaScorecardManager="collapsedPrimaryCta" data-HeroModulesManager="mediaHeroCta:click"></rt-button><watchlist-button slot="collapsedWatchlistCta" emsid="76add34d-b98d-34b5-9e0f-2eac74d2ab10" mediatype="TvSeason" mediatitle="Season 2 &ndash; Peacemaker" state="unchecked" theme="transparent-lighttext" data-HeroModulesManager="mediaHeroWatchlistBtn" data-WatchlistButtonManager="watchlistButton:click"><span slot="text">Watchlist</span></watchlist-button><score-icon-critics slot="collapsedCriticsIcon" size="2.5"></score-icon-critics><rt-text slot="collapsedCriticsScore" context="label" size="1.375"></rt-text><rt-link slot="collapsedCriticsLink" size="0.75"></rt-link><rt-text slot="collapsedCriticsLabel" size="0.75">Tomatometer</rt-text><score-icon-audience slot="collapsedAudienceIcon" size="2.5"></score-icon-audience><rt-text slot="collapsedAudienceScore" context="label" size="1.375"></rt-text><rt-link slot="collapsedAudienceLink" size="0.75"></rt-link><rt-text slot="collapsedAudienceLabel" size="0.75">Popcornmeter</rt-text></media-hero><script id="media-hero-json" data-json="mediaHero" type="application/json">{"averageColorHsl":"33,54,15","iconic":{"srcDesktop":"https://resizing.flixster.com/2jGm07y7TAmgwksV1KSg9Xsogtg=/620x336/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/166/403/thumb_47CFBBB8-95A3-4634-8B02-581F2C757D98.jpg","srcMobile":"https://resizing.flixster.com/bvZfzyIQHc8UI0CJS9UdOdRXC7w=/375x210/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/166/403/thumb_47CFBBB8-95A3-4634-8B02-581F2C757D98.jpg"},"content":{"episodeTitle":"","metadataGenres":["Comedy","Action"],"metadataProps":["Next Ep Thu Sep 11"],"posterSrc":"https://resizing.flixster.com/kNXWRTdmL5I0O6L6XKMfIje7Qaw=/68x102/v2/https://resizing.flixster.com/J-WXxwHSdv6w7VHxcCrPdebOvUA=/ems.cHJkLWVtcy1hc3NldHMvdHZzZWFzb24vNmE4MmUxOWItNGY5Yy00YzY0LTk5ODktZDY1MGEzZjdmZDFhLmpwZw==","title":"Season 2 &ndash; Peacemaker","primaryVideo":{"contentType":"PROMO","durationInSeconds":"99.933","mpxId":"2447231043621","publicId":"nTePljVEct61","thumbnail":{"url":"https://statcdn.fandango.com/MPX/image/NBCU_Fandango/166/403/thumb_47CFBBB8-95A3-4634-8B02-581F2C757D98.jpg"},"title":"Peacemaker: Season 2 Trailer - Weeks Ahead","runtime":"1:39"}}} </script></div><hero-modules-manager><script data-json="vanity" type="application/json">{"emsId":"76add34d-b98d-34b5-9e0f-2eac74d2ab10","href":"/tv/peacemaker_2022/s02","lifecycleWindow":{"date":"2025-09-11","lifecycle":"AIRING"},"title":"Season 2","type":"tvSeason","value":"s02","parents":[{"emsId":"c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed","href":"/tv/peacemaker_2022","title":"Peacemaker","type":"tvSeries","value":"peacemaker_2022"}],"mediaType":"TvSeason"}</script></hero-modules-manager></div><div id="main-wrap"><div id="modules-wrap" data-curation="drawer"><div class="media-scorecard no-border" data-adobe-id="media-scorecard" data-qa="section:media-scorecard"><media-scorecard hideaudiencescore="false" skeleton="panel" data-AdsMediaScorecardManager="mediaScorecard" data-HeroModulesManager="mediaScorecard"><rt-img alt="poster image" loading="lazy" slot="posterImage" src="https://resizing.flixster.com/icqaMuFdXKqmN8qur-dfG4I-hWs=/206x305/v2/https://resizing.flixster.com/J-WXxwHSdv6w7VHxcCrPdebOvUA=/ems.cHJkLWVtcy1hc3NldHMvdHZzZWFzb24vNmE4MmUxOWItNGY5Yy00YzY0LTk5ODktZDY1MGEzZjdmZDFhLmpwZw=="></rt-img><rt-button slot="criticsScoreIcon" data-MediaScorecardManager="overlayOpen:click" theme="transparent"><score-icon-critics certified="true" sentiment="POSITIVE" size="2.5"></score-icon-critics></rt-button><rt-text slot="criticsScore" context="label" role="button" size="1.375" data-MediaScorecardManager="overlayOpen:click">99%</rt-text><rt-text slot="criticsScoreType" class="critics-score-type" role="button" size="0.75" data-MediaScorecardManager="overlayOpen:click">Tomatometer</rt-text><rt-link slot="criticsReviews" size="0.75" href="/tv/peacemaker_2022/s02/reviews">82 Reviews </rt-link><rt-button slot="audienceScoreIcon" data-MediaScorecardManager="overlayOpen:click" theme="transparent"><score-icon-audience certified="false" size="2.5" sentiment="POSITIVE"></score-icon-audience></rt-button><rt-text slot="audienceScore" context="label" role="button" size="1.375" data-MediaScorecardManager="overlayOpen:click">80%</rt-text><rt-text slot="audienceScoreType" class="audience-score-type" role="button" size="0.75" data-MediaScorecardManager="overlayOpen:click">Popcornmeter</rt-text><rt-link slot="audienceReviews" size="0.75" href="/tv/peacemaker_2022/s02/reviews?type=user">1,000+ Ratings </rt-link><div slot="description" data-AdsMediaScorecardManager="description"><drawer-more maxlines="2" skeleton="panel" status="closed" style="--display: flex; gap: 4px;"><rt-text slot="content" size="1">A man fights for peace at any cost, no matter how many people he has to kill to get it. </rt-text><rt-link slot="ctaOpen"><rt-icon icon="down-open"></rt-icon></rt-link><rt-link slot="ctaClose"><rt-icon icon="up-open"></rt-icon></rt-link></drawer-more></div><affiliate-icon data-AdsMediaScorecardManager="affiliateIcon" icon="max-us" slot="affiliateIcon"></affiliate-icon><rt-img data-AdsMediaScorecardManager="affiliateIconCustom" slot="affiliateIconCustom" hidden></rt-img><rt-text context="label" data-AdsMediaScorecardManager="affiliatePrimaryText" size="1" slot="affiliatePrimaryText">Watch on Max</rt-text><rt-text data-AdsMediaScorecardManager="affiliateSecondaryText" size="0.75" slot="affiliateSecondaryText"></rt-text><rt-button arialabel="Stream Peacemaker &mdash; Season 2 on Max" href="https://max.prf.hn/click/camref:1100lqRUT?cmp=rt_leaderboard" rel="noopener" shape="pill" slot="affiliateCtaBtn" style="--backgroundColor: #3478C1; --textColor: #FFFFFF;" target="_blank" theme="simplified" data-AdsMediaScorecardManager="affiliateCtaBtn" data-HeroModulesManager="mediaScorecardCta:click">Stream Now </rt-button><div slot="adImpressions"></div></media-scorecard><media-scorecard-manager><script id="media-scorecard-json" data-json="mediaScorecard" type="application/json">{"audienceScore":{"averageRating":"4.1","bandedRatingCount":"1,000+ Ratings","likedCount":937,"notLikedCount":235,"reviewCount":330,"score":"80","scoreType":"ALL","sentiment":"POSITIVE","certified":false,"reviewsPageUrl":"/tv/peacemaker_2022/s02/reviews?type=user","scorePercent":"80%","title":"Popcornmeter"},"criticsScore":{"averageRating":"8.00","certified":true,"likedCount":81,"notLikedCount":1,"ratingCount":82,"reviewCount":82,"score":"99","sentiment":"POSITIVE","reviewsPageUrl":"/tv/peacemaker_2022/s02/reviews","scorePercent":"99%","title":"Tomatometer"},"criticReviewHref":"/critics/self-submission/tvSeason/76add34d-b98d-34b5-9e0f-2eac74d2ab10","cta":{"affiliate":"max-us","buttonStyle":{"backgroundColor":"#3478C1","textColor":"#FFFFFF"},"buttonText":"Stream Now","buttonAnnouncement":"Stream Peacemaker &mdash; Season 2 on Max","buttonUrl":"https://max.prf.hn/click/camref:1100lqRUT?cmp=rt_leaderboard","icon":"max-us","windowDate":"","windowText":"Watch on Max"},"description":"A man fights for peace at any cost, no matter how many people he has to kill to get it.","hideAudienceScore":false,"overlay":{"audienceAll":{"averageRating":"4.1","bandedRatingCount":"1,000+ Ratings","likedCount":937,"notLikedCount":235,"reviewCount":330,"score":"80","scoreType":"ALL","sentiment":"POSITIVE","certified":false,"reviewsPageUrl":"/tv/peacemaker_2022/s02/reviews?type=user","scorePercent":"80%","title":"Popcornmeter","scoreLinkUrl":"/tv/peacemaker_2022/s02/reviews?type=user"},"audienceTitle":"Popcornmeter","audienceVerified":{"bandedRatingCount":"0 Verified Ratings","likedCount":0,"notLikedCount":0,"reviewCount":0,"scoreType":"VERIFIED","certified":false,"title":"Popcornmeter","scoreLinkUrl":"/tv/peacemaker_2022/s02/reviews?type=verified_audience"},"criticsAll":{"averageRating":"8.00","certified":true,"likedCount":81,"notLikedCount":1,"ratingCount":82,"reviewCount":82,"score":"99","sentiment":"POSITIVE","reviewsPageUrl":"/tv/peacemaker_2022/s02/reviews","scorePercent":"99%","title":"Tomatometer","scoreLinkUrl":"/tv/peacemaker_2022/s02/reviews","scoreLinkText":"82 Reviews"},"criticsTitle":"Tomatometer","criticsTop":{"averageRating":"7.20","certified":true,"likedCount":12,"notLikedCount":1,"ratingCount":13,"reviewCount":13,"score":"92","sentiment":"POSITIVE","reviewsPageUrl":"/tv/peacemaker_2022/s02/reviews","scorePercent":"92%","title":"Tomatometer","scoreLinkUrl":"/tv/peacemaker_2022/s02/reviews?type=top_critics","scoreLinkText":"13 Top Critic Reviews"},"hasAudienceAll":true,"hasAudienceVerified":false,"hasCriticsAll":true,"hasCriticsTop":true,"mediaType":"TvSeason","showScoreDetailsAudience":true,"learnMoreUrl":"https://editorial.rottentomatoes.com/article/introducing-verified-audience-score/"},"primaryImageUrl":"https://resizing.flixster.com/icqaMuFdXKqmN8qur-dfG4I-hWs=/206x305/v2/https://resizing.flixster.com/J-WXxwHSdv6w7VHxcCrPdebOvUA=/ems.cHJkLWVtcy1hc3NldHMvdHZzZWFzb24vNmE4MmUxOWItNGY5Yy00YzY0LTk5ODktZDY1MGEzZjdmZDFhLmpwZw=="} </script></media-scorecard-manager></div><section class="modules-nav" data-ModulesNavigationManager="navWrap"><modules-navigation-manager></modules-navigation-manager><nav><modules-navigation-carousel skeleton="panel" tilewidth="auto" data-ModulesNavigationManager="navCarousel"><a slot="tile" href="#where-to-watch"><rt-tab data-ModulesNavigationManager="navTab">Where to Watch</rt-tab></a><a slot="tile" href="#what-to-know"><rt-tab data-ModulesNavigationManager="navTab">What to Know</rt-tab></a><a slot="tile" href="#critics-reviews"><rt-tab data-ModulesNavigationManager="navTab">Reviews</rt-tab></a><a slot="tile" href="#cast-and-crew"><rt-tab data-ModulesNavigationManager="navTab">Cast &amp; Crew</rt-tab></a><a slot="tile" href="#episodes"><rt-tab data-ModulesNavigationManager="navTab">Episodes</rt-tab></a><a slot="tile" href="#more-like-this"><rt-tab data-ModulesNavigationManager="navTab">More Like This</rt-tab></a><a slot="tile" href="#news-and-guides"><rt-tab data-ModulesNavigationManager="navTab">Related News</rt-tab></a><a slot="tile" href="#videos"><rt-tab data-ModulesNavigationManager="navTab">Videos</rt-tab></a><a slot="tile" href="#photos"><rt-tab data-ModulesNavigationManager="navTab">Photos</rt-tab></a><a slot="tile" href="#media-info"><rt-tab data-ModulesNavigationManager="navTab">Media Info</rt-tab></a></modules-navigation-carousel></nav></section><div class="modules-layout" tabindex="-1" data-ModulesNavigationManager="content:focusin"><div id="where-to-watch" class="dom-anchor" data-ModulesNavigationManager="domAnchor"></div><section aria-labelledby="where-to-watch-label" class="where-to-watch" data-adobe-id="where-to-watch" data-qa="section:where-to-watch"><div class="header-wrap"><h2 class="unset" id="where-to-watch-label"><rt-text context="heading" size="1.25" style="--textTransform: capitalize;">Where to Watch</rt-text></h2><h3 class="unset"><rt-text context="heading" size="0.75" style="--textColor: var(--grayDark4); --letterSpacing: 1px; --textTransform: capitalize;">Peacemaker &mdash; Season 2 </rt-text></h3></div><where-to-watch-manager><script id="where-to-watch-json" data-json="whereToWatch" type="application/json">{"affiliates":[{"icon":"max-us","url":"https://max.prf.hn/click/camref:1100lqRUT?cmp=rt_where_to_watch","isSponsoredLink":true,"text":"Max"}],"affiliatesText":"Watch Peacemaker &mdash; Season 2 with a subscription on Max.","justWatchMediaType":"show","seasonNumber":"2","showtimesUrl":"","releaseYear":"2022","tarsSlug":"rt-affiliates-sort-order","title":"Peacemaker &mdash; Season 2"} </script></where-to-watch-manager><div hidden data-WhereToWatchManager="jwContainer"></div><div hidden data-WhereToWatchManager="w2wContainer"><carousel-slider data-curation="rt-affiliates-sort-order" gap="15px" skeleton="panel" tile-width="80px" exclude-page-indicators><where-to-watch-meta affiliate="max-us" data-qa="affiliate-item" href="https://max.prf.hn/click/camref:1100lqRUT?cmp=rt_where_to_watch" issponsoredlink="true" skeleton="panel" slot="tile"><where-to-watch-bubble image="max-us" slot="bubble" tabindex="-1"></where-to-watch-bubble><span slot="license">Max</span><span slot="coverage"></span></where-to-watch-meta></carousel-slider><p class="affiliates-text">Watch Peacemaker &mdash; Season 2 with a subscription on Max. </p></div></section></div><div class="modules-layout" tabindex="-1" data-ModulesNavigationManager="content:focusin"><div id="what-to-know" class="dom-anchor" data-ModulesNavigationManager="domAnchor"></div><section aria-labelledby="what-to-know-label" class="what-to-know" data-adobe-id="what-to-know" data-qa="section:what-to-know"><div class="header-wrap"><rt-text context="heading" size="0.75" style="--textColor: var(--grayDark4); --letterSpacing: 1px;--textTransform: capitalize;">Peacemaker &mdash; Season 2 </rt-text><h2 class="unset" id="what-to-know-label"><rt-text context="heading" size="1.25" style="--textTransform: capitalize;">What to Know</rt-text></h2></div><div class="content"><div id="critics-consensus" class="consensus"><rt-text context="heading"><score-icon-critics certified="true" sentiment="POSITIVE" size="1"></score-icon-critics>Critics Consensus </rt-text><p><em>Peacemaker</em>'s second season goes multidimensional while still maintaining a singular focus on emotional stakes, seamlessly transporting this outrageous antihero into a fresh cinematic universe.</p><a href="/tv/peacemaker_2022/s02/reviews">Read Critics Reviews</a></div></div></section></div><ad-unit hidden unit-display="desktop" unit-type="opbannerone"><div slot="ad-inject" class="banner-ad"></div></ad-unit><ad-unit hidden unit-display="mobile" unit-type="interscroller" no-retry data-AdUnitManager="adUnit:interscrollerinstantiated"><aside slot="ad-inject" class="center mobile-interscroller"></aside></ad-unit><div class="modules-layout" tabindex="-1" data-ModulesNavigationManager="content:focusin"><div id="critics-reviews" class="dom-anchor" data-ModulesNavigationManager="domAnchor"></div><section aria-labelledby="critics-reviews-label" class="critics-reviews" data-adobe-id="critics-reviews" data-qa="section:critics-reviews"><div class="header-wrap"><h2 class="unset" id="critics-reviews-label"><rt-text size="1.25" context="heading" data-qa="title">Critics Reviews</rt-text></h2><rt-button arialabel="Critics Reviews" data-qa="view-all-link" href="/tv/peacemaker_2022/s02/reviews" shape="pill" size="0.875" style="--buttonPadding: 6px 24px;--backgroundColor: var(--grayLight1);--borderColor: transparent;--letterSpacing: 1px;" theme="light">View More (82) </rt-button></div><div class="content-wrap"><carousel-slider tile-width="80%,45%" skeleton="panel" data-qa="carousel"><media-review-card-critic slot="tile" istopreview="true" data-qa="critic-review-tile"><rt-link aria-hidden="true" href="/critics/craig-mathieson" slot="displayImage" style="--textColor: var(--grayDark2)" tabindex="-1" data-qa="critic-picture"><img src="https://resizing.flixster.com/srE4lzd1NPKRCAex2t-o3qKCZ_I=/fit-in/128x128/v2/https://resizing.flixster.com/EebuNkttDjHuP0eJJntXvIB0Vsk=/128x128/v1.YzszMzA2O2o7MjAzNDA7MjA0ODszMDA7MzAw" alt="Critic's profile" /></rt-link><rt-link href="/critics/craig-mathieson" slot="displayName" style="--textColor: var(--grayDark2)" data-qa="critic-link"><rt-text context="label" size="0.875" style="--lineHeight: 1.25; --textColor: var(--grayDark3);">Craig Mathieson </rt-text></rt-link><rt-link href="/critics/source/2041" slot="publicationName" style="--textColor: var(--grayDark2)" data-qa="source-link"><rt-text size="0.75">The Age (Australia) </rt-text></rt-link><drawer-more maxlines="6" skeleton="panel" slot="reviewQuote" status="closed"><rt-text size="0.875" slot="content" data-qa="review-text">Dimensional travel, vengeful relatives and spurts of bloody violence set the tone for a series that remains a cartoonish change of pace. </rt-text></drawer-more><score-icon-critics sentiment="POSITIVE" size="1" slot="scoreIcon" verticalalign="sub"></score-icon-critics><rt-text size="0.75" slot="originalScore" style="--textColor: #62686F" data-qa="review-rating"><span>Rated: 3/5</span></rt-text><rt-text size="0.75" slot="createDate" style="--textColor: #62686F" data-qa="review-date"><span>Aug 28, 2025 </span></rt-text><rt-link href="https://www.theage.com.au/culture/tv-and-radio/it-s-implausible-and-ludicrous-but-this-netflix-thriller-is-held-together-by-a-terrific-performance-20250822-p5mp0a.html" slot="editorialUrl" size="0.875" target="_blank" data-qa="full-review-link">Full Review</rt-link></media-review-card-critic><media-review-card-critic slot="tile" istopreview="true" data-qa="critic-review-tile"><rt-link aria-hidden="true" href="/critics/graeme-virtue" slot="displayImage" style="--textColor: var(--grayDark2)" tabindex="-1" data-qa="critic-picture"><img src="https://images.fandango.com/cms/assets/5b6ff500-1663-11ec-ae31-05a670d2d590--rtactordefault.png" alt="Critic's profile" /></rt-link><rt-link href="/critics/graeme-virtue" slot="displayName" style="--textColor: var(--grayDark2)" data-qa="critic-link"><rt-text context="label" size="0.875" style="--lineHeight: 1.25; --textColor: var(--grayDark3);">Graeme Virtue </rt-text></rt-link><rt-link href="/critics/source/205" slot="publicationName" style="--textColor: var(--grayDark2)" data-qa="source-link"><rt-text size="0.75">Guardian </rt-text></rt-link><drawer-more maxlines="6" skeleton="panel" slot="reviewQuote" status="closed"><rt-text size="0.875" slot="content" data-qa="review-text">Gunn clearly loved these damaged characters enough to carry them over to his shiny new universe, but he is also never afraid to put them through the wringer. It makes even the tiniest victories in their lives feel momentous. </rt-text></drawer-more><score-icon-critics sentiment="POSITIVE" size="1" slot="scoreIcon" verticalalign="sub"></score-icon-critics><rt-text size="0.75" slot="originalScore" style="--textColor: #62686F" data-qa="review-rating"><span>Rated: 4/5</span></rt-text><rt-text size="0.75" slot="createDate" style="--textColor: #62686F" data-qa="review-date"><span>Aug 25, 2025 </span></rt-text><rt-link href="https://www.theguardian.com/tv-and-radio/2025/aug/23/peacemaker-season-two-review-the-orgy-scene-feels-like-a-tv-first" slot="editorialUrl" size="0.875" target="_blank" data-qa="full-review-link">Full Review</rt-link></media-review-card-critic><media-review-card-critic slot="tile" istopreview="true" data-qa="critic-review-tile"><rt-link aria-hidden="true" href="/critics/sam-adams" slot="displayImage" style="--textColor: var(--grayDark2)" tabindex="-1" data-qa="critic-picture"><img src="https://resizing.flixster.com/uK77X4Dq-KlH0QD9rPlgcBuqvpo=/fit-in/128x128/v2/https://resizing.flixster.com/69Az0Lkh0DgEFbanVrfrUIgEkEU=/128x128/v1.YzsxMDAwMDAzMzc3O2o7MjAzOTQ7MjA0ODs0NDg4OzQzNDM" alt="Critic's profile" /></rt-link><rt-link href="/critics/sam-adams" slot="displayName" style="--textColor: var(--grayDark2)" data-qa="critic-link"><rt-text context="label" size="0.875" style="--lineHeight: 1.25; --textColor: var(--grayDark3);">Sam Adams </rt-text></rt-link><rt-link href="/critics/source/419" slot="publicationName" style="--textColor: var(--grayDark2)" data-qa="source-link"><rt-text size="0.75">Slate </rt-text></rt-link><drawer-more maxlines="6" skeleton="panel" slot="reviewQuote" status="closed"><rt-text size="0.875" slot="content" data-qa="review-text">Like the first season, the second season of Peacemaker risks stretching too few jokes over too many hoursโ€”even if sometimes the joke going on for too long is the joke. But itโ€™s also strangely fascinating. </rt-text></drawer-more><score-icon-critics sentiment="POSITIVE" size="1" slot="scoreIcon" verticalalign="sub"></score-icon-critics><rt-text size="0.75" slot="originalScore" style="--textColor: #62686F" data-qa="review-rating"><span></span></rt-text><rt-text size="0.75" slot="createDate" style="--textColor: #62686F" data-qa="review-date"><span>Aug 25, 2025 </span></rt-text><rt-link href="https://slate.com/culture/2025/08/peacemaker-season-2-superman-2025-james-gunn-hbo-max.html" slot="editorialUrl" size="0.875" target="_blank" data-qa="full-review-link">Full Review</rt-link></media-review-card-critic><media-review-card-critic slot="tile" istopreview="false" data-qa="critic-review-tile"><rt-link aria-hidden="true" href="/critics/david-craig" slot="displayImage" style="--textColor: var(--grayDark2)" tabindex="-1" data-qa="critic-picture"><img src="https://images.fandango.com/cms/assets/5b6ff500-1663-11ec-ae31-05a670d2d590--rtactordefault.png" alt="Critic's profile" /></rt-link><rt-link href="/critics/david-craig" slot="displayName" style="--textColor: var(--grayDark2)" data-qa="critic-link"><rt-text context="label" size="0.875" style="--lineHeight: 1.25; --textColor: var(--grayDark3);">David Craig </rt-text></rt-link><rt-link href="/critics/source/2290" slot="publicationName" style="--textColor: var(--grayDark2)" data-qa="source-link"><rt-text size="0.75">Radio Times </rt-text></rt-link><drawer-more maxlines="6" skeleton="panel" slot="reviewQuote" status="closed"><rt-text size="0.875" slot="content" data-qa="review-text">From the opening dance number, it&#39;s an enormous amount of fun that carefully balances its surreal pleasures with impactful character-led moments โ€“ and plenty of unexpected twists. </rt-text></drawer-more><score-icon-critics sentiment="POSITIVE" size="1" slot="scoreIcon" verticalalign="sub"></score-icon-critics><rt-text size="0.75" slot="originalScore" style="--textColor: #62686F" data-qa="review-rating"><span>Rated: 4/5</span></rt-text><rt-text size="0.75" slot="createDate" style="--textColor: #62686F" data-qa="review-date"><span>Aug 30, 2025 </span></rt-text><rt-link href="https://www.radiotimes.com/tv/sci-fi/peacemaker-season-2-review/" slot="editorialUrl" size="0.875" target="_blank" data-qa="full-review-link">Full Review</rt-link></media-review-card-critic><media-review-card-critic slot="tile" istopreview="false" data-qa="critic-review-tile"><rt-link aria-hidden="true" href="/critics/alex-maidy" slot="displayImage" style="--textColor: var(--grayDark2)" tabindex="-1" data-qa="critic-picture"><img src="https://resizing.flixster.com/MFK0dnw8QU4wEbVsa7vgkOQixvA=/fit-in/128x128/v2/https://resizing.flixster.com/4FX6vSszdhdJXA0VeDjGk6plUQU=/128x128/v1.YzszMzk5O2o7MjAzNDA7MjA0ODszMDA7MzAw" alt="Critic's profile" /></rt-link><rt-link href="/critics/alex-maidy" slot="displayName" style="--textColor: var(--grayDark2)" data-qa="critic-link"><rt-text context="label" size="0.875" style="--lineHeight: 1.25; --textColor: var(--grayDark3);">Alex Maidy </rt-text></rt-link><rt-link href="/critics/source/573" slot="publicationName" style="--textColor: var(--grayDark2)" data-qa="source-link"><rt-text size="0.75">JoBlo&#39;s Movie Network </rt-text></rt-link><drawer-more maxlines="6" skeleton="panel" slot="reviewQuote" status="closed"><rt-text size="0.875" slot="content" data-qa="review-text">While I do not suffer from bird blindness, I can say that Peacemaker soars with the ducks. </rt-text></drawer-more><score-icon-critics sentiment="POSITIVE" size="1" slot="scoreIcon" verticalalign="sub"></score-icon-critics><rt-text size="0.75" slot="originalScore" style="--textColor: #62686F" data-qa="review-rating"><span>Rated: 9/10</span></rt-text><rt-text size="0.75" slot="createDate" style="--textColor: #62686F" data-qa="review-date"><span>Aug 28, 2025 </span></rt-text><rt-link href="https://www.joblo.com/peacemaker-season-2-tv-review-james-gunn-and-john-cena-reunite-for-a-brutal-and-fun-soft-reboot-into-the-dcu/" slot="editorialUrl" size="0.875" target="_blank" data-qa="full-review-link">Full Review</rt-link></media-review-card-critic><media-review-card-critic slot="tile" istopreview="false" data-qa="critic-review-tile"><rt-link aria-hidden="true" href="/critics/joel-keller" slot="displayImage" style="--textColor: var(--grayDark2)" tabindex="-1" data-qa="critic-picture"><img src="https://images.fandango.com/cms/assets/5b6ff500-1663-11ec-ae31-05a670d2d590--rtactordefault.png" alt="Critic's profile" /></rt-link><rt-link href="/critics/joel-keller" slot="displayName" style="--textColor: var(--grayDark2)" data-qa="critic-link"><rt-text context="label" size="0.875" style="--lineHeight: 1.25; --textColor: var(--grayDark3);">Joel Keller </rt-text></rt-link><rt-link href="/critics/source/2701" slot="publicationName" style="--textColor: var(--grayDark2)" data-qa="source-link"><rt-text size="0.75">Decider </rt-text></rt-link><drawer-more maxlines="6" skeleton="panel" slot="reviewQuote" status="closed"><rt-text size="0.875" slot="content" data-qa="review-text">Peacemaker continues to be a funny but emotional superhero drama, with a surprisingly effective performance by Cena at its center, with a fun-to-watch ensemble around him. </rt-text></drawer-more><score-icon-critics sentiment="POSITIVE" size="1" slot="scoreIcon" verticalalign="sub"></score-icon-critics><rt-text size="0.75" slot="originalScore" style="--textColor: #62686F" data-qa="review-rating"><span></span></rt-text><rt-text size="0.75" slot="createDate" style="--textColor: #62686F" data-qa="review-date"><span>Aug 25, 2025 </span></rt-text><rt-link href="https://decider.com/2025/08/21/peacemaker-season-2-hbo-max-review/" slot="editorialUrl" size="0.875" target="_blank" data-qa="full-review-link">Full Review</rt-link></media-review-card-critic><tile-view-more aspect="fill" background="mediaHero" slot="tile"><rt-button href="/tv/peacemaker_2022/s02/reviews" shape="pill" theme="transparent-lighttext">Read all reviews </rt-button></tile-view-more></carousel-slider></div></section></div><section aria-labelledby="audience-reviews-label" class="audience-reviews" data-adobe-id="audience-reviews" data-qa="section:audience-reviews"><div class="header-wrap"><h2 class="unset" id="audience-reviews-label"><rt-text size="1.25" context="heading" data-qa="title">Audience Reviews</rt-text></h2><rt-button arialabel="Audience Reviews" class="" data-qa="view-all-link" href="/tv/peacemaker_2022/s02/reviews?type=user" shape="pill" size="0.875" style="--buttonPadding: 6px 24px;--backgroundColor: var(--grayLight1);--borderColor: transparent;--letterSpacing: 1px;" theme="light">View More (330) </rt-button></div><div class="content-wrap"><carousel-slider tile-width="80%,45%" skeleton="panel" data-qa="carousel"><media-review-card-audience slot="tile" isverifiedreview="false" data-qa="audience-review-tile"><rt-link context="label" href="/profiles/L2LHQkcloTPnTolIpYFABto4hvvCZqhWGunaCGju99CAYixdirosyLCmmCyXu1lC2WSzDHPPCJeHq8sWoFb2hg1svlhqrhnrSBRFM9Fv4tL8" size="0.875" slot="displayName" style="--letterSpacing: 1px; --textColor: var(--grayDark3);" data-qa="user-name">Andres G </rt-link><drawer-more maxlines="4" skeleton="panel" slot="reviewQuote" status="closed"><rt-text size="0.875" slot="content" data-qa="review-text">No es mala pero tampoco no es una de las mejores series como dice la critica </rt-text></drawer-more><rt-text size="0.75" slot="originalScore" data-qa="review-rating"><span aria-hidden="true">Rated 3.5/5 Stars &bull;&nbsp;</span><sr-text>Rated 3.5 out of 5 stars</sr-text></rt-text><rt-text size="0.75" slot="createDate" style="--textColor: #62686F" data-qa="review-date"><span>09/07/25</span></rt-text><rt-link data-MediaAudienceReviewsManager="fullReviewBtn:click" data-rating-id="0c9c0287-f018-4c40-9eba-187b1bd08e78" size="0.875" slot="fullReviewBtn" data-qa="full-review-btn">Full Review </rt-link></media-review-card-audience><media-review-card-audience slot="tile" isverifiedreview="false" data-qa="audience-review-tile"><rt-link context="label" href="/profiles/1K0Tevfx1fpAixzs6Psdjf2mIQQC2vTgjFJPTB6SNNC2xiB8IzYIWvfvvCkaIJ0uK2FnyCzzCbXu9JUnGsBnHG4SOrfbnukxcxpTplcAVFjr" size="0.875" slot="displayName" style="--letterSpacing: 1px; --textColor: var(--grayDark3);" data-qa="user-name">Jude C </rt-link><drawer-more maxlines="4" skeleton="panel" slot="reviewQuote" status="closed"><rt-text size="0.875" slot="content" data-qa="review-text">Absolute-cinema, it&#39;s too bad we can&#39;t see peacemaker though </rt-text></drawer-more><rt-text size="0.75" slot="originalScore" data-qa="review-rating"><span aria-hidden="true">Rated 5/5 Stars &bull;&nbsp;</span><sr-text>Rated 5 out of 5 stars</sr-text></rt-text><rt-text size="0.75" slot="createDate" style="--textColor: #62686F" data-qa="review-date"><span>09/07/25</span></rt-text><rt-link data-MediaAudienceReviewsManager="fullReviewBtn:click" data-rating-id="2e3b3a6a-4748-4d87-8588-0e13b2c9f56f" size="0.875" slot="fullReviewBtn" data-qa="full-review-btn">Full Review </rt-link></media-review-card-audience><media-review-card-audience slot="tile" isverifiedreview="false" data-qa="audience-review-tile"><rt-link context="label" href="/profiles/RkbsJQIovuYyFnRS2bUWRHByHAACVQtlMtB4Fl2hnnCx6iQVSD9HvwuZZC6viXKFRXFlPuGGCvNFPaILkIZ2fY4FKxu9GiZLI9ySWNikyiWO" size="0.875" slot="displayName" style="--letterSpacing: 1px; --textColor: var(--grayDark3);" data-qa="user-name">ลukasz S </rt-link><drawer-more maxlines="4" skeleton="panel" slot="reviewQuote" status="closed"><rt-text size="0.875" slot="content" data-qa="review-text">Good </rt-text></drawer-more><rt-text size="0.75" slot="originalScore" data-qa="review-rating"><span aria-hidden="true">Rated 5/5 Stars &bull;&nbsp;</span><sr-text>Rated 5 out of 5 stars</sr-text></rt-text><rt-text size="0.75" slot="createDate" style="--textColor: #62686F" data-qa="review-date"><span>09/06/25</span></rt-text><rt-link data-MediaAudienceReviewsManager="fullReviewBtn:click" data-rating-id="7dc00af0-0d3c-4f70-9080-2c6482151b67" size="0.875" slot="fullReviewBtn" data-qa="full-review-btn">Full Review </rt-link></media-review-card-audience><media-review-card-audience slot="tile" isverifiedreview="false" data-qa="audience-review-tile"><rt-link context="label" href="/profiles/0x6umbFAQuYLSnpiGgS6ji4bseeCaAuk8szYTdLIyyCk4izaioaIjns44CK1F1aFZeTDefvvCvwtkBhNPcNos0rhDpULQuzlf4xuwdu1NIg8" size="0.875" slot="displayName" style="--letterSpacing: 1px; --textColor: var(--grayDark3);" data-qa="user-name">Fernando N </rt-link><drawer-more maxlines="4" skeleton="panel" slot="reviewQuote" status="closed"><rt-text size="0.875" slot="content" data-qa="review-text">Essa segunda temporada estรก me surpreendendo positivamente, a quรญmica dos personagens estรฃo incrรญveis, cenas de aรงรฃo perfeitas e as participaรงรตes surpreendentes </rt-text></drawer-more><rt-text size="0.75" slot="originalScore" data-qa="review-rating"><span aria-hidden="true">Rated 5/5 Stars &bull;&nbsp;</span><sr-text>Rated 5 out of 5 stars</sr-text></rt-text><rt-text size="0.75" slot="createDate" style="--textColor: #62686F" data-qa="review-date"><span>09/06/25</span></rt-text><rt-link data-MediaAudienceReviewsManager="fullReviewBtn:click" data-rating-id="cf1c9119-24f9-4cfe-8e43-103aba7c899b" size="0.875" slot="fullReviewBtn" data-qa="full-review-btn">Full Review </rt-link></media-review-card-audience><media-review-card-audience slot="tile" isverifiedreview="false" data-qa="audience-review-tile"><rt-link context="label" href="/profiles/MlqtDxizBCpKHPQFrQfbjuxWFXXCNxsz4CJAhAlHLLCVJi9dSMRHvaFQQCMjt6jiKDFBGIZZCMgfAnH8KslqtadHVRiZBHlBhmdIPxCNPuOJ" size="0.875" slot="displayName" style="--letterSpacing: 1px; --textColor: var(--grayDark3);" data-qa="user-name">Shan V </rt-link><drawer-more maxlines="4" skeleton="panel" slot="reviewQuote" status="closed"><rt-text size="0.875" slot="content" data-qa="review-text">starting to introduce old characters like theyre somehow important to the DCU, thge comedy puts me off and reminds me of the beginnings of Marvel shows where they started out good then went to crap. </rt-text></drawer-more><rt-text size="0.75" slot="originalScore" data-qa="review-rating"><span aria-hidden="true">Rated 0.5/5 Stars &bull;&nbsp;</span><sr-text>Rated 0.5 out of 5 stars</sr-text></rt-text><rt-text size="0.75" slot="createDate" style="--textColor: #62686F" data-qa="review-date"><span>09/07/25</span></rt-text><rt-link data-MediaAudienceReviewsManager="fullReviewBtn:click" data-rating-id="6b5b496f-f34c-4a9f-84ae-68bed827c3fb" size="0.875" slot="fullReviewBtn" data-qa="full-review-btn">Full Review </rt-link></media-review-card-audience><media-review-card-audience slot="tile" isverifiedreview="false" data-qa="audience-review-tile"><rt-link context="label" href="/profiles/kOBhdziqwfJjf9eSb0SxJIBpiDDCKPulVIpGcoGirrCZqiWgFNVI9NhggCXDFv9s18uKbIPPCKOHvNfRoFAwsbKtmwI90IwAuJ0FPZTWmT8y" size="0.875" slot="displayName" style="--letterSpacing: 1px; --textColor: var(--grayDark3);" data-qa="user-name">Zubair A </rt-link><drawer-more maxlines="4" skeleton="panel" slot="reviewQuote" status="closed"><rt-text size="0.875" slot="content" data-qa="review-text">With Gunn at the helm, the mission continues: flawless action, killer music, and jokes that hit like a helmet-beam. Fabulous doesn&#39;t even cover it. </rt-text></drawer-more><rt-text size="0.75" slot="originalScore" data-qa="review-rating"><span aria-hidden="true">Rated 5/5 Stars &bull;&nbsp;</span><sr-text>Rated 5 out of 5 stars</sr-text></rt-text><rt-text size="0.75" slot="createDate" style="--textColor: #62686F" data-qa="review-date"><span>09/05/25</span></rt-text><rt-link data-MediaAudienceReviewsManager="fullReviewBtn:click" data-rating-id="62b62ce6-5eb3-4a5a-a0b9-d94e47069a0d" size="0.875" slot="fullReviewBtn" data-qa="full-review-btn">Full Review </rt-link></media-review-card-audience><tile-view-more aspect="fill" background="mediaHero" slot="tile"><rt-button href="/tv/peacemaker_2022/s02/reviews?type=user" shape="pill" theme="transparent-lighttext">Read all reviews </rt-button></tile-view-more></carousel-slider></div><media-audience-reviews-manager><script type="application/json" data-json="reviewsData">{"audienceScore":{"reviewCount":330,"score":"80","sentiment":"POSITIVE","certified":false,"scorePercent":"80%"},"criticsScore":{"certified":true,"score":"99","sentiment":"POSITIVE","scorePercent":"99%"},"emptyMessage":"There are no Audience reviews for Peacemaker &mdash; Season 2 yet.","linkCss":"","partial":"pages/_shared/mediaAudienceReviewsCarousel.html","ratingsData":{"emsId":"76add34d-b98d-34b5-9e0f-2eac74d2ab10","isPreRelease":false},"reviews":[{"displayDate":"09/07/25","displayName":"Andres G","isVerified":false,"ratingId":"0c9c0287-f018-4c40-9eba-187b1bd08e78","ratingRange":"Rated 3.5/5 Stars &bull;&nbsp;","ratingRangeA11y":"Rated 3.5 out of 5 stars","review":"No es mala pero tampoco no es una de las mejores series como dice la critica","userAccountLink":"/profiles/L2LHQkcloTPnTolIpYFABto4hvvCZqhWGunaCGju99CAYixdirosyLCmmCyXu1lC2WSzDHPPCJeHq8sWoFb2hg1svlhqrhnrSBRFM9Fv4tL8"},{"displayDate":"09/07/25","displayName":"Jude C","isVerified":false,"ratingId":"2e3b3a6a-4748-4d87-8588-0e13b2c9f56f","ratingRange":"Rated 5/5 Stars &bull;&nbsp;","ratingRangeA11y":"Rated 5 out of 5 stars","review":"Absolute-cinema, it's too bad we can't see peacemaker though ","userAccountLink":"/profiles/1K0Tevfx1fpAixzs6Psdjf2mIQQC2vTgjFJPTB6SNNC2xiB8IzYIWvfvvCkaIJ0uK2FnyCzzCbXu9JUnGsBnHG4SOrfbnukxcxpTplcAVFjr"},{"displayDate":"09/06/25","displayName":"ลukasz S","isVerified":false,"ratingId":"7dc00af0-0d3c-4f70-9080-2c6482151b67","ratingRange":"Rated 5/5 Stars &bull;&nbsp;","ratingRangeA11y":"Rated 5 out of 5 stars","review":"\n\n\n\n\n\n\n\n\n\n\n\n\nGood\n\n\n\n","userAccountLink":"/profiles/RkbsJQIovuYyFnRS2bUWRHByHAACVQtlMtB4Fl2hnnCx6iQVSD9HvwuZZC6viXKFRXFlPuGGCvNFPaILkIZ2fY4FKxu9GiZLI9ySWNikyiWO"},{"displayDate":"09/06/25","displayName":"Fernando N","isVerified":false,"ratingId":"cf1c9119-24f9-4cfe-8e43-103aba7c899b","ratingRange":"Rated 5/5 Stars &bull;&nbsp;","ratingRangeA11y":"Rated 5 out of 5 stars","review":"Essa segunda temporada estรก me surpreendendo positivamente, a quรญmica dos personagens estรฃo incrรญveis, cenas de aรงรฃo perfeitas e as participaรงรตes surpreendentes ","userAccountLink":"/profiles/0x6umbFAQuYLSnpiGgS6ji4bseeCaAuk8szYTdLIyyCk4izaioaIjns44CK1F1aFZeTDefvvCvwtkBhNPcNos0rhDpULQuzlf4xuwdu1NIg8"},{"displayDate":"09/07/25","displayName":"Shan V","isVerified":false,"ratingId":"6b5b496f-f34c-4a9f-84ae-68bed827c3fb","ratingRange":"Rated 0.5/5 Stars &bull;&nbsp;","ratingRangeA11y":"Rated 0.5 out of 5 stars","review":"starting to introduce old characters like theyre somehow important to the DCU, thge comedy puts me off and reminds me of the beginnings of Marvel shows where they started out good then went to crap.","userAccountLink":"/profiles/MlqtDxizBCpKHPQFrQfbjuxWFXXCNxsz4CJAhAlHLLCVJi9dSMRHvaFQQCMjt6jiKDFBGIZZCMgfAnH8KslqtadHVRiZBHlBhmdIPxCNPuOJ"},{"displayDate":"09/05/25","displayName":"Zubair A","isVerified":false,"ratingId":"62b62ce6-5eb3-4a5a-a0b9-d94e47069a0d","ratingRange":"Rated 5/5 Stars &bull;&nbsp;","ratingRangeA11y":"Rated 5 out of 5 stars","review":"With Gunn at the helm, the mission continues: flawless action, killer music, and jokes that hit like a helmet-beam. Fabulous doesn't even cover it.","userAccountLink":"/profiles/kOBhdziqwfJjf9eSb0SxJIBpiDDCKPulVIpGcoGirrCZqiWgFNVI9NhggCXDFv9s18uKbIPPCKOHvNfRoFAwsbKtmwI90IwAuJ0FPZTWmT8y"}],"reviewCount":330,"reviewsUrl":"/tv/peacemaker_2022/s02/reviews?type=user","title":"Peacemaker &mdash; Season 2","viewMoreText":"View More (330)"}</script></media-audience-reviews-manager></section><section aria-labelledby="rate-and-review-label" class="rate-and-review" data-adobe-id="rate-and-review" data-qa="section:rate-and-review"><rate-and-review-module-manager><script data-json="rateAndReviewModule" type="application/json">{"emsId":"76add34d-b98d-34b5-9e0f-2eac74d2ab10","releaseDate":"Sep 11, 2025","mediaType":"tvSeason","title":"Peacemaker &mdash; Season 2"}</script></rate-and-review-module-manager><div class="header-wrap"><rt-text context="heading" size="0.75" style="--textColor: #62686F; --letterSpacing: 1px; --textTransform: capitalize;">Peacemaker &mdash; Season 2 </rt-text><h2 class="unset" id="rate-and-review-label"><rt-text size="1.25" context="heading">My Rating</rt-text></h2></div><div class="content"><rate-and-review-module data-RateAndReviewModuleManager="rateAndReviewModule" skeleton="panel" status="unrated"><rating-stars-group data-RateAndReviewModuleManager="stars:changed" data-RateAndReviewOverlayManager="moduleStars" aria-labelledby="ratingStarsLabel" is-selectable size="2.75,2" slot="rating"></rating-stars-group><rating-descriptions context="label" data-RateAndReviewModuleManager="ratingDescriptions" size="1" slot="description" hidden></rating-descriptions><drawer-more maxlines="2" slot="review-quote" status="closed"><rt-text data-RateAndReviewModuleManager="userReview" data-RateAndReviewOverlayManager="moduleReview" size="0.875" slot="content"></rt-text><rt-link slot="ctaOpen" size="0.875" context="label">Read More</rt-link><rt-link slot="ctaClose" size="0.875" context="label">Read Less</rt-link></drawer-more><rt-button data-RateAndReviewModuleManager="rateBtn:click" shape="pill" size="1" slot="cta-rate">POST RATING </rt-button><rt-button data-RateAndReviewModuleManager="writeReviewBtn:click" size="1" slot="cta-review" theme="transparent">WRITE A REVIEW </rt-button><rt-button data-RateAndReviewModuleManager="editReviewBtn:click" size="1" slot="cta-edit" theme="transparent">EDIT REVIEW </rt-button></rate-and-review-module></div><rate-and-review-overlay-manager data-RateAndReviewModuleManager="overlayManager:error,success"></rate-and-review-overlay-manager></section><div class="modules-layout" tabindex="-1" data-ModulesNavigationManager="content:focusin"><div id="cast-and-crew" class="dom-anchor" data-ModulesNavigationManager="domAnchor"></div><section aria-labelledby="cast-and-crew-label" class="cast-and-crew" data-adobe-id="cast-and-crew" data-qa="section:cast-and-crew"><div class="header-wrap"><h2 class="unset" id="cast-and-crew-label"><rt-text size="1.25" context="heading" data-qa="title">Cast & Crew</rt-text></h2><rt-button arialabel="Cast and Crew" data-qa="view-all-link" href="/tv/peacemaker_2022/s02/cast-and-crew" shape="pill" size="0.875" style="--buttonPadding: 6px 24px;--backgroundColor: var(--grayLight1);--borderColor: transparent;--letterSpacing: 1px;" theme="light">View All </rt-button></div><div class="content-wrap"><a href="/celebrity/john_cena" data-qa="person-item"><tile-dynamic skeleton="panel"><rt-img alt="John Cena thumbnail image" aria-hidden="true" loading="lazy" slot="image" src="https://resizing.flixster.com/qFr2ZK1qYDkqSmM5eT3nz_n6E_g=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/487578_v9_ba.jpg"></rt-img><div slot="insetText" aria-label="John Cena, Peacemaker"><p class="name" data-qa="person-name">John Cena</p><p class="role" data-qa="person-role">Peacemaker</p></div></tile-dynamic></a><a href="/celebrity/danielle_brooks" data-qa="person-item"><tile-dynamic skeleton="panel"><rt-img alt="Danielle Brooks thumbnail image" aria-hidden="true" loading="lazy" slot="image" src="https://resizing.flixster.com/KhnY5vsfjM0vtw0cZL3aNxXbeUE=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/765589_v9_bc.jpg"></rt-img><div slot="insetText" aria-label="Danielle Brooks, Leota Adebayo"><p class="name" data-qa="person-name">Danielle Brooks</p><p class="role" data-qa="person-role">Leota Adebayo</p></div></tile-dynamic></a><a href="/celebrity/freddie_stroma" data-qa="person-item"><tile-dynamic skeleton="panel"><rt-img alt="Freddie Stroma thumbnail image" aria-hidden="true" loading="lazy" slot="image" src="https://resizing.flixster.com/Yk2eiDCtamfmNlK-xMa7nmEw_Po=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/GNLZZGG00283ZZD.jpg"></rt-img><div slot="insetText" aria-label="Freddie Stroma, Vigilante"><p class="name" data-qa="person-name">Freddie Stroma</p><p class="role" data-qa="person-role">Vigilante</p></div></tile-dynamic></a><a href="/celebrity/chukwudi_iwuji" data-qa="person-item"><tile-dynamic skeleton="panel"><rt-img alt="Chukwudi Iwuji thumbnail image" aria-hidden="true" loading="lazy" slot="image" src="https://resizing.flixster.com/uNAFlG9dNMjJwyMbPDiCsbjkX8I=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/565157_v9_ba.jpg"></rt-img><div slot="insetText" aria-label="Chukwudi Iwuji, Clemson Murn"><p class="name" data-qa="person-name">Chukwudi Iwuji</p><p class="role" data-qa="person-role">Clemson Murn</p></div></tile-dynamic></a><a href="/celebrity/jennifer_holland" data-qa="person-item"><tile-dynamic skeleton="panel"><rt-img alt="Jennifer Holland thumbnail image" aria-hidden="true" loading="lazy" slot="image" src="https://resizing.flixster.com/-xeYAf0O7fGIQHRx_YkL7vnaMMg=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/331642_v9_bb.jpg"></rt-img><div slot="insetText" aria-label="Jennifer Holland, Emilia Harcourt"><p class="name" data-qa="person-name">Jennifer Holland</p><p class="role" data-qa="person-role">Emilia Harcourt</p></div></tile-dynamic></a><a href="/celebrity/steve_agee" data-qa="person-item"><tile-dynamic skeleton="panel"><rt-img alt="Steve Agee thumbnail image" aria-hidden="true" loading="lazy" slot="image" src="https://resizing.flixster.com/YprPSg0SXNIqq-Wy4UEz4ovBnOw=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/223358_v9_bd.jpg"></rt-img><div slot="insetText" aria-label="Steve Agee, John Economos"><p class="name" data-qa="person-name">Steve Agee</p><p class="role" data-qa="person-role">John Economos</p></div></tile-dynamic></a></div></section></div><div class="modules-layout" tabindex="-1" data-ModulesNavigationManager="content:focusin"><div id="episodes" class="dom-anchor" data-ModulesNavigationManager="domAnchor"></div><section aria-labelledby="episodes-label" class="episodes" data-adobe-id="episodes" data-qa="section:episodes"><div class="header-wrap"><h2 class="unset" id="episodes-label"><rt-text size="1.25" context="heading" data-qa="title">Episodes</rt-text></h2></div><div class="content-wrap"><carousel-slider tile-width="240px" skeleton="panel" data-qa="carousel"><tile-episode slot="tile" href="/tv/peacemaker_2022/s02/e01" skeleton="panel" data-qa="episode-tile"><rt-img src="https://resizing.flixster.com/ADuuwJmNs1LzDj9ZWi3mBkG5mHA=/370x208/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317941_e_h10_aa.jpg" alt="Episode 1 video still" slot="image" aria-hidden="true"></rt-img><rt-text size="0.75" context="label" slot="episode" data-qa="episode-label">Episode 1</rt-text><rt-text size="0.75" slot="airDate" data-qa="episode-air-date">Aired Aug 21, 2025</rt-text><rt-text size="1" context="label" slot="title" data-qa="episode-title">The Ties That Grind</rt-text><rt-text size="0.875" slot="description" data-qa="episode-description">While Peacemaker attempts to join the Justice Gang, Harcourt struggles to find work, and Economos takes on a challenging new assignment.</rt-text><rt-text size="0.875" context="label" slot="details" data-qa="episode-details">Details <rt-icon icon="right-chevron"></rt-icon></rt-text></tile-episode><tile-episode slot="tile" href="/tv/peacemaker_2022/s02/e02" skeleton="panel" data-qa="episode-tile"><rt-img src="https://resizing.flixster.com/6rSe6JCrjz0NuuMSQfEO3pCsr40=/370x208/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317945_e_h8_aa.jpg" alt="Episode 2 video still" slot="image" aria-hidden="true"></rt-img><rt-text size="0.75" context="label" slot="episode" data-qa="episode-label">Episode 2</rt-text><rt-text size="0.75" slot="airDate" data-qa="episode-air-date">Aired Aug 28, 2025</rt-text><rt-text size="1" context="label" slot="title" data-qa="episode-title">A Man Is Only as Good as His Bird</rt-text><rt-text size="0.875" slot="description" data-qa="episode-description">As Economos clashes with his new handler, Peacemaker must deal with the consequences of his actions in the alternate dimension.</rt-text><rt-text size="0.875" context="label" slot="details" data-qa="episode-details">Details <rt-icon icon="right-chevron"></rt-icon></rt-text></tile-episode><tile-episode slot="tile" href="/tv/peacemaker_2022/s02/e03" skeleton="panel" data-qa="episode-tile"><rt-img src="https://resizing.flixster.com/sNxCjYecSXn3aStn3ym5vaqmXDI=/370x208/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_i_h10_aa.jpg" alt="Episode 3 video still" slot="image" aria-hidden="true"></rt-img><rt-text size="0.75" context="label" slot="episode" data-qa="episode-label">Episode 3</rt-text><rt-text size="0.75" slot="airDate" data-qa="episode-air-date">Aired Sep 4, 2025</rt-text><rt-text size="1" context="label" slot="title" data-qa="episode-title">Another Rick Up My Sleeve</rt-text><rt-text size="0.875" slot="description" data-qa="episode-description">A man fights for peace at any cost, no matter how many people he has to kill to get it.</rt-text><rt-text size="0.875" context="label" slot="details" data-qa="episode-details">Details <rt-icon icon="right-chevron"></rt-icon></rt-text></tile-episode><tile-episode slot="tile" href="/tv/peacemaker_2022/s02/e04" skeleton="panel" data-qa="episode-tile"><rt-img src="https://resizing.flixster.com/sNxCjYecSXn3aStn3ym5vaqmXDI=/370x208/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_i_h10_aa.jpg" alt="Episode 4 video still" slot="image" aria-hidden="true"></rt-img><rt-text size="0.75" context="label" slot="episode" data-qa="episode-label">Episode 4</rt-text><rt-text size="0.75" slot="airDate" data-qa="episode-air-date">Airs Thu Sep 11</rt-text><rt-text size="1" context="label" slot="title" data-qa="episode-title">Need I Say Door</rt-text><rt-text size="0.875" slot="description" data-qa="episode-description">A man fights for peace at any cost, no matter how many people he has to kill to get it.</rt-text><rt-text size="0.875" context="label" slot="details" data-qa="episode-details">Details <rt-icon icon="right-chevron"></rt-icon></rt-text></tile-episode><tile-episode slot="tile" href="/tv/peacemaker_2022/s02/e05" skeleton="panel" data-qa="episode-tile"><rt-img src="https://resizing.flixster.com/sNxCjYecSXn3aStn3ym5vaqmXDI=/370x208/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_i_h10_aa.jpg" alt="Episode 5 video still" slot="image" aria-hidden="true"></rt-img><rt-text size="0.75" context="label" slot="episode" data-qa="episode-label">Episode 5</rt-text><rt-text size="0.75" slot="airDate" data-qa="episode-air-date">Airs Thu Sep 18</rt-text><rt-text size="1" context="label" slot="title" data-qa="episode-title">Back to the Suture</rt-text><rt-text size="0.875" slot="description" data-qa="episode-description">A man fights for peace at any cost, no matter how many people he has to kill to get it.</rt-text><rt-text size="0.875" context="label" slot="details" data-qa="episode-details">Details <rt-icon icon="right-chevron"></rt-icon></rt-text></tile-episode><tile-episode slot="tile" href="/tv/peacemaker_2022/s02/e06" skeleton="panel" data-qa="episode-tile"><rt-img src="https://resizing.flixster.com/sNxCjYecSXn3aStn3ym5vaqmXDI=/370x208/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_i_h10_aa.jpg" alt="Episode 6 video still" slot="image" aria-hidden="true"></rt-img><rt-text size="0.75" context="label" slot="episode" data-qa="episode-label">Episode 6</rt-text><rt-text size="0.75" slot="airDate" data-qa="episode-air-date">Airs Thu Sep 25</rt-text><rt-text size="1" context="label" slot="title" data-qa="episode-title">Ignorance is Chris</rt-text><rt-text size="0.875" slot="description" data-qa="episode-description">A man fights for peace at any cost, no matter how many people he has to kill to get it.</rt-text><rt-text size="0.875" context="label" slot="details" data-qa="episode-details">Details <rt-icon icon="right-chevron"></rt-icon></rt-text></tile-episode><tile-episode slot="tile" href="/tv/peacemaker_2022/s02/e07" skeleton="panel" data-qa="episode-tile"><rt-img src="https://resizing.flixster.com/sNxCjYecSXn3aStn3ym5vaqmXDI=/370x208/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_i_h10_aa.jpg" alt="Episode 7 video still" slot="image" aria-hidden="true"></rt-img><rt-text size="0.75" context="label" slot="episode" data-qa="episode-label">Episode 7</rt-text><rt-text size="0.75" slot="airDate" data-qa="episode-air-date">Airs Thu Oct 2</rt-text><rt-text size="1" context="label" slot="title" data-qa="episode-title"></rt-text><rt-text size="0.875" slot="description" data-qa="episode-description">A man fights for peace at any cost, no matter how many people he has to kill to get it.</rt-text><rt-text size="0.875" context="label" slot="details" data-qa="episode-details">Details <rt-icon icon="right-chevron"></rt-icon></rt-text></tile-episode><tile-episode slot="tile" href="/tv/peacemaker_2022/s02/e08" skeleton="panel" data-qa="episode-tile"><rt-img src="https://resizing.flixster.com/sNxCjYecSXn3aStn3ym5vaqmXDI=/370x208/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_i_h10_aa.jpg" alt="Episode 8 video still" slot="image" aria-hidden="true"></rt-img><rt-text size="0.75" context="label" slot="episode" data-qa="episode-label">Episode 8</rt-text><rt-text size="0.75" slot="airDate" data-qa="episode-air-date">Airs Thu Oct 9</rt-text><rt-text size="1" context="label" slot="title" data-qa="episode-title"></rt-text><rt-text size="0.875" slot="description" data-qa="episode-description">A man fights for peace at any cost, no matter how many people he has to kill to get it.</rt-text><rt-text size="0.875" context="label" slot="details" data-qa="episode-details">Details <rt-icon icon="right-chevron"></rt-icon></rt-text></tile-episode></carousel-slider></div></section></div><div class="modules-layout" tabindex="-1" data-ModulesNavigationManager="content:focusin"><div id="more-like-this" class="dom-anchor" data-ModulesNavigationManager="domAnchor"></div><section aria-labelledby="more-like-this-label" class="more-like-this" data-adobe-id="more-like-this" data-qa="section:more-like-this"><div class="header-wrap"><div class="link-wrap"><h3 class="unset" id="more-like-this-label"><rt-text size="1.25" context="heading">More Like This </rt-text></h3><rt-button arialabel="Popular TV on Streaming" data-qa="view-all-link" href="/browse/tv_series_browse/sort:popular" shape="pill" size="0.875" style="--buttonPadding: 6px 24px;--backgroundColor: var(--grayLight1);--borderColor: transparent;--letterSpacing: 1px;" theme="light">View All </rt-button></div></div><div class="content-wrap"><carousel-slider skeleton="panel" tile-width="140px" gap="15px"><tile-poster-card slot="tile"><rt-link slot="primaryImage" href="/tv/twisted_metal" tabindex="-1"><sr-text>Twisted Metal</sr-text><rt-img loading="" src="https://resizing.flixster.com/MtyzaFnLaDY2B3SeRCOm91EwADE=/206x305/v2/https://resizing.flixster.com/mAAW4s6Bzl9wVHeH6GYXImTQFYY=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvNWNlYzRjODUtYzU0OS00NzJhLTk5NmQtOTgwOTg1MTlkYWJjLmpwZw==" alt="Twisted Metal poster"></rt-img></rt-link><score-icon-critics certified="false" sentiment="POSITIVE" size="1" slot="criticsIcon" verticalalign="sub"></score-icon-critics><rt-text slot="criticsScore" size="0.9" context="label">79% </rt-text><score-icon-audience certified="false" sentiment="POSITIVE" size="1" slot="audienceIcon"></score-icon-audience><rt-text slot="audienceScore" size="0.9" context="label">88% </rt-text><rt-link slot="title" href="/tv/twisted_metal" size="0.85" context="label">Twisted Metal </rt-link><watchlist-button data-WatchlistButtonManager="watchlistButton:click" emsid="80499e3c-a069-3e48-9d23-5b72d9f58079" mediatype="TvSeries" mediatitle="Twisted Metal" slot="watchlistButton" state="unchecked"><span slot="text">Watchlist</span></watchlist-button><rt-button data-content-type="PROMO" data-disable-ads="" data-ems-id="80499e3c-a069-3e48-9d23-5b72d9f58079" data-mpx-id="2438157891760" data-position="1" data-public-id="sGoBrIyuO6Gy" data-title="Twisted Metal: Season 2 Trailer" data-track="poster" data-type="TvSeries" data-VideoPlayerOverlayManager="btnVideo:click" data-video-list="" slot="trailerButton" size="0.875" theme="transparent"><rt-icon icon="play"></rt-icon><span>TRAILER</span><sr-text>for Twisted Metal</sr-text></rt-button></tile-poster-card><tile-poster-card slot="tile"><rt-link slot="primaryImage" href="/tv/comrade_detective" tabindex="-1"><sr-text>Comrade Detective</sr-text><rt-img loading="" src="https://resizing.flixster.com/L44TL1O_i8N47QRPZ1DpAjipU78=/206x305/v2/https://resizing.flixster.com/sUXscZBjGl80M7C8wEX9qISu3Ls=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvUlRUVjI1OTA1OC53ZWJw" alt="Comrade Detective poster"></rt-img></rt-link><score-icon-critics certified="false" sentiment="POSITIVE" size="1" slot="criticsIcon" verticalalign="sub"></score-icon-critics><rt-text slot="criticsScore" size="0.9" context="label">85% </rt-text><score-icon-audience certified="false" sentiment="POSITIVE" size="1" slot="audienceIcon"></score-icon-audience><rt-text slot="audienceScore" size="0.9" context="label">88% </rt-text><rt-link slot="title" href="/tv/comrade_detective" size="0.85" context="label">Comrade Detective </rt-link><watchlist-button data-WatchlistButtonManager="watchlistButton:click" emsid="bcb4d43d-3dbc-3da9-8051-51454a471ea1" mediatype="TvSeries" mediatitle="Comrade Detective" slot="watchlistButton" state="unchecked"><span slot="text">Watchlist</span></watchlist-button></tile-poster-card><tile-poster-card slot="tile"><rt-link slot="primaryImage" href="/tv/saul_of_the_mole_men" tabindex="-1"><sr-text>Saul of the Mole Men</sr-text><rt-img loading="" src="https://resizing.flixster.com/vvIDMwPetdv8iLBHM9DCici60ag=/206x305/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p284327_b_v8_ab.jpg" alt="Saul of the Mole Men poster"></rt-img></rt-link><score-icon-critics certified="false" sentiment="NEGATIVE" size="1" slot="criticsIcon" verticalalign="sub"></score-icon-critics><rt-text slot="criticsScore" size="0.9" context="label">17% </rt-text><score-icon-audience certified="false" sentiment="" size="1" slot="audienceIcon"></score-icon-audience><rt-text slot="audienceScore" size="0.9" context="label">% </rt-text><rt-link slot="title" href="/tv/saul_of_the_mole_men" size="0.85" context="label">Saul of the Mole Men </rt-link><watchlist-button data-WatchlistButtonManager="watchlistButton:click" emsid="19f88158-8f52-3e02-b11e-89f20b21eb4e" mediatype="TvSeries" mediatitle="Saul of the Mole Men" slot="watchlistButton" state="unchecked"><span slot="text">Watchlist</span></watchlist-button></tile-poster-card><tile-poster-card slot="tile"><rt-link slot="primaryImage" href="/tv/somebody_somewhere" tabindex="-1"><sr-text>Somebody Somewhere</sr-text><rt-img loading="" src="https://resizing.flixster.com/TJvPpdNIt4ic6QlG5Kwgkf80PZo=/206x305/v2/https://resizing.flixster.com/v8tcpv_dwS6GbygTvvUXubTn9_w=/ems.cHJkLWVtcy1hc3NldHMvdHZzZXJpZXMvNTNiNTUwMzUtMzQyNi00NGM3LTkzNTgtMjU0NzU2MGU4NmE4LmpwZw==" alt="Somebody Somewhere poster"></rt-img></rt-link><score-icon-critics certified="false" sentiment="POSITIVE" size="1" slot="criticsIcon" verticalalign="sub"></score-icon-critics><rt-text slot="criticsScore" size="0.9" context="label">100% </rt-text><score-icon-audience certified="false" sentiment="POSITIVE" size="1" slot="audienceIcon"></score-icon-audience><rt-text slot="audienceScore" size="0.9" context="label">93% </rt-text><rt-link slot="title" href="/tv/somebody_somewhere" size="0.85" context="label">Somebody Somewhere </rt-link><watchlist-button data-WatchlistButtonManager="watchlistButton:click" emsid="5cddaabc-0c8d-3e41-b44b-c44487f54cc9" mediatype="TvSeries" mediatitle="Somebody Somewhere" slot="watchlistButton" state="unchecked"><span slot="text">Watchlist</span></watchlist-button><rt-button data-content-type="PROMO" data-disable-ads="" data-ems-id="5cddaabc-0c8d-3e41-b44b-c44487f54cc9" data-mpx-id="2378416707669" data-position="4" data-public-id="wnDPGb8gSEC7" data-title="Somebody Somewhere: Season 3 Trailer" data-track="poster" data-type="TvSeries" data-VideoPlayerOverlayManager="btnVideo:click" data-video-list="" slot="trailerButton" size="0.875" theme="transparent"><rt-icon icon="play"></rt-icon><span>TRAILER</span><sr-text>for Somebody Somewhere</sr-text></rt-button></tile-poster-card><tile-poster-card slot="tile"><rt-link slot="primaryImage" href="/tv/south_side" tabindex="-1"><sr-text>South Side</sr-text><rt-img loading="" src="none" alt="South Side poster"></rt-img></rt-link><score-icon-critics certified="false" sentiment="POSITIVE" size="1" slot="criticsIcon" verticalalign="sub"></score-icon-critics><rt-text slot="criticsScore" size="0.9" context="label">100% </rt-text><score-icon-audience certified="false" sentiment="POSITIVE" size="1" slot="audienceIcon"></score-icon-audience><rt-text slot="audienceScore" size="0.9" context="label">92% </rt-text><rt-link slot="title" href="/tv/south_side" size="0.85" context="label">South Side </rt-link><watchlist-button data-WatchlistButtonManager="watchlistButton:click" emsid="6942346f-6675-39af-945a-1c6c6d526cef" mediatype="TvSeries" mediatitle="South Side" slot="watchlistButton" state="unchecked"><span slot="text">Watchlist</span></watchlist-button><rt-button data-content-type="PROMO" data-disable-ads="" data-ems-id="6942346f-6675-39af-945a-1c6c6d526cef" data-mpx-id="2130332739528" data-position="5" data-public-id="RFlaFZLoP7L5" data-title="South Side: Season 3 Trailer" data-track="poster" data-type="TvSeries" data-VideoPlayerOverlayManager="btnVideo:click" data-video-list="" slot="trailerButton" size="0.875" theme="transparent"><rt-icon icon="play"></rt-icon><span>TRAILER</span><sr-text>for South Side</sr-text></rt-button></tile-poster-card><tile-poster-card skeleton="panel" slot="tile" tabindex="-1"><tile-view-more aspect="posterCard" background="collage" slot="primaryImage"></tile-view-more><rt-text slot="title" size="0.85" context="label">Discover more movies and TV shows.</rt-text><rt-button href="/browse/tv_series_browse/sort:popular" slot="watchlistButton" shape="pill" size="0.875" theme="transparent-darktext" aria-label="View More Popular TV on Streaming">View More </rt-button></tile-poster-card></carousel-slider></div></section></div><div class="modules-layout" tabindex="-1" data-ModulesNavigationManager="content:focusin"><div id="news-and-guides" class="dom-anchor" data-ModulesNavigationManager="domAnchor"></div><section aria-labelledby="news-and-guides-label" class="news-and-guides" data-adobe-id="news-and-guides" data-qa="section:news-and-guides"><div class="header-wrap"><div class="link-wrap"><h2 class="unset" id="news-and-guides-label"><rt-text size="1.25" style="--textTransform: capitalize;" context="heading" data-qa="title">Related TV News</rt-text></h2><rt-button arialabel="Related TV News" data-qa="view-all-link" href="https://editorial.rottentomatoes.com/more-related-content/?relatedtvseasonid=76add34d-b98d-34b5-9e0f-2eac74d2ab10" shape="pill" size="0.875" style="--buttonPadding: 6px 24px;--backgroundColor: var(--grayLight1);--borderColor: transparent;--letterSpacing: 1px;" theme="light">View All </rt-button></div></div><div class="content-wrap"><carousel-slider tile-width="80%,240px" skeleton="panel" data-qa="carousel"><a slot="tile" href="https://editorial.rottentomatoes.com/article/what-to-expect-in-peacemaker-season-2/" data-qa="article"><tile-dynamic orientation="landscape" skeleton="panel"><rt-img slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2025/08/Peacemaker_S2_Preview-Rep.jpg" loading="lazy"></rt-img><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="article-title">What To Expect In <em>Peacemaker</em>: Season 2</rt-text></drawer-more></tile-dynamic></a><a slot="tile" href="https://editorial.rottentomatoes.com/article/peacemaker-season-2-first-reviews/" data-qa="article"><tile-dynamic orientation="landscape" skeleton="panel"><rt-img slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2025/08/Peacemaker_S2_Reviews-Rep.jpg" loading="lazy"></rt-img><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="article-title"><em>Peacemaker</em>: Season 2 First Reviews: Even Better Than the First Season</rt-text></drawer-more></tile-dynamic></a><a slot="tile" href="https://editorial.rottentomatoes.com/article/6-tv-and-streaming-shows-you-should-binge-watch-in-august-2025/" data-qa="article"><tile-dynamic orientation="landscape" skeleton="panel"><rt-img slot="image" src="https://editorial.rottentomatoes.com/wp-content/uploads/2025/07/600KingOfTheHill.jpg" loading="lazy"></rt-img><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="article-title">6 TV and Streaming Shows You Should Binge-Watch in August</rt-text></drawer-more></tile-dynamic></a></carousel-slider></div></section></div><div class="modules-layout" tabindex="-1" data-ModulesNavigationManager="content:focusin"><div id="videos" class="dom-anchor" data-ModulesNavigationManager="domAnchor"></div><section aria-labelledby="videos-carousel-label" class="videos-carousel" data-adobe-id="videos-carousel" data-qa="section:videos-carousel"><div class="header-wrap"><div class="link-wrap"><h2 class="unset" data-qa="videos-section-title" id="videos-carousel-label"><rt-text size="1.25" context="heading">Videos</rt-text></h2><rt-button arialabel=" videos" data-qa="videos-view-all-link" href="/tv/peacemaker_2022/s02/videos" shape="pill" size="0.875" style="--buttonPadding: 6px 24px;--backgroundColor: var(--grayLight1);--borderColor: transparent;--letterSpacing: 1px;" theme="light">View All </rt-button></div><h3 class="unset"><rt-text context="heading" size="0.75" style="--letterSpacing: 1px; --textColor: var(--grayDark4); --textTransform: capitalize;">Peacemaker &mdash; Season 2 </rt-text></h3></div><carousel-slider tile-width="80%,240px" data-VideosCarouselManager="carousel" skeleton="panel" data-qa="videos-carousel"><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="" src="https://resizing.flixster.com/VXMtl5AJHPunHrflqzrZ6NFP9Pg=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/590/227/thumb_35B8F85D-E0B5-42E6-983D-064D4B953DB2.jpg" alt="Setting up the DCU in &#39;Peacemaker&#39; Season 2"></rt-img><rt-button theme="transparent" data-ems-id="76add34d-b98d-34b5-9e0f-2eac74d2ab10" data-mpx-id="2447675459785" data-public-id="P47wMJdbExRa" data-type="TvSeason" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">Setting up the DCU in &#39;Peacemaker&#39; Season 2</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">Setting up the DCU in &#39;Peacemaker&#39; Season 2</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">1:06 </rt-badge></tile-video><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="" src="https://resizing.flixster.com/Q9pHL0plKwChI7M2x8OIXc4tTmY=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/166/403/thumb_47CFBBB8-95A3-4634-8B02-581F2C757D98.jpg" alt="Peacemaker: Season 2 Trailer - Weeks Ahead"></rt-img><rt-button theme="transparent" data-ems-id="76add34d-b98d-34b5-9e0f-2eac74d2ab10" data-mpx-id="2447231043621" data-public-id="nTePljVEct61" data-type="TvSeason" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">Peacemaker: Season 2 Trailer - Weeks Ahead</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">Peacemaker: Season 2 Trailer - Weeks Ahead</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">1:39 </rt-badge></tile-video><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="" src="https://resizing.flixster.com/xwQEohhLNSYlXEPpuV8X_yi_hwc=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/206/519/thumb_2CDEC1BA-4D54-4C43-B824-2101B8C0A29D.jpg" alt="Peacemaker: Season 2 Opening Title Sequence"></rt-img><rt-button theme="transparent" data-ems-id="76add34d-b98d-34b5-9e0f-2eac74d2ab10" data-mpx-id="2446199363799" data-public-id="evKz2_ikqufb" data-type="TvSeason" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">Peacemaker: Season 2 Opening Title Sequence</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">Peacemaker: Season 2 Opening Title Sequence</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">1:44 </rt-badge></tile-video><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="" src="https://resizing.flixster.com/Xzrp5pHl-edpsJO7vPi4qyvliTE=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/20/983/thumb_4C16B48F-A4A5-4574-BA92-74DDB42BA687.jpg" alt="James Gunn on Setting Up the DCU in &#39;Peacemaker&#39; Season 2"></rt-img><rt-button theme="transparent" data-ems-id="76add34d-b98d-34b5-9e0f-2eac74d2ab10" data-mpx-id="2446004803886" data-public-id="TkqRtSTVgnbQ" data-type="TvSeason" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">James Gunn on Setting Up the DCU in &#39;Peacemaker&#39; Season 2</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">James Gunn on Setting Up the DCU in &#39;Peacemaker&#39; Season 2</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">6:22 </rt-badge></tile-video><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="" src="https://resizing.flixster.com/uHrBotX26H8cgnZBr_y9pQrDfik=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/959/599/thumb_AE4DD3A1-45A3-463E-8C12-F066951D541A.jpg" alt="Peacemaker: Season 2 Red Band Trailer"></rt-img><rt-button theme="transparent" data-ems-id="76add34d-b98d-34b5-9e0f-2eac74d2ab10" data-mpx-id="2444841539922" data-public-id="LulHILmxo0GT" data-type="TvSeason" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">Peacemaker: Season 2 Red Band Trailer</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">Peacemaker: Season 2 Red Band Trailer</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">1:50 </rt-badge></tile-video><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="lazy" src="https://resizing.flixster.com/RMadHxyLW7kRTN9v-8qX6O1J1Xc=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/232/199/thumb_2df88d50-6d52-11f0-94b5-022bbbb30d69.jpg" alt="&quot;The Action is RAW&quot; in Peacemaker"></rt-img><rt-button theme="transparent" data-ems-id="76add34d-b98d-34b5-9e0f-2eac74d2ab10" data-mpx-id="2441931331754" data-public-id="oIlPPGDZ6QFL" data-type="TvSeason" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">&quot;The Action is RAW&quot; in Peacemaker</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">&quot;The Action is RAW&quot; in Peacemaker</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">0:40 </rt-badge></tile-video><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="lazy" src="https://resizing.flixster.com/dMSMK3JXt3jKxRn8NrBPVCF1PAA=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/231/222/thumb_43a9e00b-6d51-11f0-94b5-022bbbb30d69.jpg" alt="Who is the Best Dancer of the Peacemaker Cast?"></rt-img><rt-button theme="transparent" data-ems-id="76add34d-b98d-34b5-9e0f-2eac74d2ab10" data-mpx-id="2441930307547" data-public-id="UoDWEtXCKY3z" data-type="TvSeason" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">Who is the Best Dancer of the Peacemaker Cast?</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">Who is the Best Dancer of the Peacemaker Cast?</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">0:37 </rt-badge></tile-video><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="lazy" src="https://resizing.flixster.com/fGPwOoH4zfWppKW5ek_DOu19_Xs=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/228/795/thumb_49a4c021-6d50-11f0-94b5-022bbbb30d69.jpg" alt="The Peacemaker Cast Chaotically Answering Questions at Comic-Con"></rt-img><rt-button theme="transparent" data-ems-id="76add34d-b98d-34b5-9e0f-2eac74d2ab10" data-mpx-id="2441927747862" data-public-id="Hrjp79IuY8Xc" data-type="TvSeason" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">The Peacemaker Cast Chaotically Answering Questions at Comic-Con</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">The Peacemaker Cast Chaotically Answering Questions at Comic-Con</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">0:35 </rt-badge></tile-video><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="lazy" src="https://resizing.flixster.com/7QkPu9-3aC8vNURefGfPXhs_aos=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/225/367/thumb_64f2bff7-6cd6-11f0-94b5-022bbbb30d69.jpg" alt="Which Peacemaker Cast Trains the Most?"></rt-img><rt-button theme="transparent" data-ems-id="76add34d-b98d-34b5-9e0f-2eac74d2ab10" data-mpx-id="2441924163802" data-public-id="p6gq9KoxjIy3" data-type="TvSeason" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">Which Peacemaker Cast Trains the Most?</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">Which Peacemaker Cast Trains the Most?</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">1:03 </rt-badge></tile-video><tile-video skeleton="panel" slot="tile" data-qa="video-item"><rt-img slot="image" fallbacktheme="iconic" loading="lazy" src="https://resizing.flixster.com/RAosXaJ6r3usacVXZxPcQnljAq8=/270x160/v2/https://statcdn.fandango.com/MPX/image/NBCU_Fandango/722/515/thumb_272BCC1C-0440-4916-A769-AFCD1706047D.jpg" alt="John Cena on His Superman CAMEO"></rt-img><rt-button theme="transparent" data-ems-id="76add34d-b98d-34b5-9e0f-2eac74d2ab10" data-mpx-id="2441371715987" data-public-id="OE0x6E5VPuGQ" data-type="TvSeason" data-VideoPlayerOverlayManager="btnVideo:click" slot="imageAction" data-qa="video-trailer-play-btn"><span class="sr-only">John Cena on His Superman CAMEO</span></rt-button><drawer-more slot="caption" maxlines="2" status="closed"><rt-text slot="content" size="1" context="label" data-qa="video-item-title">John Cena on His Superman CAMEO</rt-text></drawer-more><rt-badge slot="imageInsetLabel" theme="gray">0:39 </rt-badge></tile-video><tile-view-more aspect="landscape" background="mediaHero" slot="tile"><rt-button href="/tv/peacemaker_2022/s02/videos" shape="pill" theme="transparent-lighttext">View more videos </rt-button></tile-view-more></carousel-slider></section></div><div class="modules-layout" tabindex="-1" data-ModulesNavigationManager="content:focusin"><div id="photos" class="dom-anchor" data-ModulesNavigationManager="domAnchor"></div><section aria-labelledby="photos-carousel-label" class="photos-carousel" data-adobe-id="photos-carousel" data-qa="section:photos-carousel"><div class="header-wrap"><div class="link-wrap"><h2 class="unset" id="photos-carousel-label"><rt-text size="1.25" context="heading">Photos</rt-text></h2><rt-button arialabel="Peacemaker &mdash; Season 2 photos" data-qa="photos-view-all-link" href="/tv/peacemaker_2022/s02/pictures" shape="pill" size="0.875" style="--buttonPadding: 6px 24px;--backgroundColor: var(--grayLight1);--borderColor: transparent;--letterSpacing: 1px;" theme="light">View All </rt-button></div><h3 class="unset"><rt-text context="label" size="0.75" style="--textColor: var(--grayDark4);">Peacemaker &mdash; Season 2 </rt-text></h3></div><carousel-slider tile-width="80%,240px" skeleton="panel"><tile-photo data-PhotosCarouselManager="tilePhoto:click" slot="tile" skeleton="panel"><span class="sr-only"></span><rt-img slot="image" loading="" src="https://resizing.flixster.com/aYP8sS3MeHqIi7UB7CxcM7l8cnI=/fit-in/352x330/v2/https://resizing.flixster.com/J-WXxwHSdv6w7VHxcCrPdebOvUA=/ems.cHJkLWVtcy1hc3NldHMvdHZzZWFzb24vNmE4MmUxOWItNGY5Yy00YzY0LTk5ODktZDY1MGEzZjdmZDFhLmpwZw==,https://resizing.flixster.com/hQfmEHhCDArIWR697_2cL8dyEeY=/fit-in/705x460/v2/https://resizing.flixster.com/J-WXxwHSdv6w7VHxcCrPdebOvUA=/ems.cHJkLWVtcy1hc3NldHMvdHZzZWFzb24vNmE4MmUxOWItNGY5Yy00YzY0LTk5ODktZDY1MGEzZjdmZDFhLmpwZw==" alt="Peacemaker &amp;mdash; Season 2 photo 1"></rt-img></tile-photo><tile-photo data-PhotosCarouselManager="tilePhoto:click" slot="tile" skeleton="panel"><span class="sr-only"></span><rt-img slot="image" loading="" src="https://resizing.flixster.com/DRQvcw1LVxWuAdBlV0VnBC4biVg=/fit-in/352x330/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_b_h9_aa.jpg,https://resizing.flixster.com/N68GZ9kea8OaNmRYUvnhmnoGyz4=/fit-in/705x460/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_b_h9_aa.jpg" alt="Peacemaker &amp;mdash; Season 2 photo 2"></rt-img></tile-photo><tile-photo data-PhotosCarouselManager="tilePhoto:click" slot="tile" skeleton="panel"><span class="sr-only"></span><rt-img slot="image" loading="" src="https://resizing.flixster.com/OqHcKlrfhRfnfjOd6-de39um0Pg=/fit-in/352x330/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_b_v10_aa.jpg,https://resizing.flixster.com/Ck_we47bpcheYGfm4DspUvqCjXA=/fit-in/705x460/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_b_v10_aa.jpg" alt="Peacemaker &amp;mdash; Season 2 photo 3"></rt-img></tile-photo><tile-photo data-PhotosCarouselManager="tilePhoto:click" slot="tile" skeleton="panel"><span class="sr-only"></span><rt-img slot="image" loading="" src="https://resizing.flixster.com/B14xZt1JRPCgEqEA4fhGzfIhf0g=/fit-in/352x330/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_b_h8_aa.jpg,https://resizing.flixster.com/A6W_i7si2yuFTF58CVundvm0IWs=/fit-in/705x460/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_b_h8_aa.jpg" alt="Peacemaker &amp;mdash; Season 2 photo 4"></rt-img></tile-photo><tile-photo data-PhotosCarouselManager="tilePhoto:click" slot="tile" skeleton="panel"><span class="sr-only">Peacemaker</span><rt-img slot="image" loading="" src="https://resizing.flixster.com/7XyiXhn9BvpEhslyQpBn9hnXFr8=/fit-in/352x330/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_i_h9_aa.jpg,https://resizing.flixster.com/31A9AIs1ehVt4dSwvr3oIq1P7J0=/fit-in/705x460/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_i_h9_aa.jpg" alt="Peacemaker"></rt-img></tile-photo><tile-photo data-PhotosCarouselManager="tilePhoto:click" slot="tile" skeleton="panel"><span class="sr-only">Peacemaker</span><rt-img slot="image" loading="lazy" src="https://resizing.flixster.com/BUCSSlBKkuZW7VLv3TBVqaOQ1zg=/fit-in/352x330/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_i_v9_aa.jpg,https://resizing.flixster.com/U46-rgA8JKxNatvWQXOk2-75az0=/fit-in/705x460/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_i_v9_aa.jpg" alt="Peacemaker"></rt-img></tile-photo><tile-photo data-PhotosCarouselManager="tilePhoto:click" slot="tile" skeleton="panel"><span class="sr-only">Peacemaker</span><rt-img slot="image" loading="lazy" src="https://resizing.flixster.com/JiJ_5M6yk4RwL_eH8tnQn9m39tI=/fit-in/352x330/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_i_v8_aa.jpg,https://resizing.flixster.com/3bicZDYLXsHZV-OzWHo6IVenCc4=/fit-in/705x460/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_i_v8_aa.jpg" alt="Peacemaker"></rt-img></tile-photo><tile-photo data-PhotosCarouselManager="tilePhoto:click" slot="tile" skeleton="panel"><span class="sr-only">Peacemaker</span><rt-img slot="image" loading="lazy" src="https://resizing.flixster.com/0twCfBybv6oYrFbstg0CuOgDgew=/fit-in/352x330/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_i_h10_aa.jpg,https://resizing.flixster.com/vm58l9EGV6aj90uTvHW8usYGsrA=/fit-in/705x460/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_i_h10_aa.jpg" alt="Peacemaker"></rt-img></tile-photo><tile-view-more aspect="square,landscape" background="mediaHero" slot="tile"><rt-button href="/tv/peacemaker_2022/s02/pictures" shape="pill" theme="transparent-lighttext" aria-label="View more Peacemaker &mdash; Season 2 photos">View more photos </rt-button></tile-view-more></carousel-slider><photos-carousel-manager><script id="photosCarousel" type="application/json" hidden>{"title":"Peacemaker &mdash; Season 2","images":[{"aspectRatio":"ASPECT_RATIO_2_3","height":"1920","width":"1296","imageUrl":"https://resizing.flixster.com/hQfmEHhCDArIWR697_2cL8dyEeY=/fit-in/705x460/v2/https://resizing.flixster.com/J-WXxwHSdv6w7VHxcCrPdebOvUA=/ems.cHJkLWVtcy1hc3NldHMvdHZzZWFzb24vNmE4MmUxOWItNGY5Yy00YzY0LTk5ODktZDY1MGEzZjdmZDFhLmpwZw==","imageUrlMobile":"https://resizing.flixster.com/aYP8sS3MeHqIi7UB7CxcM7l8cnI=/fit-in/352x330/v2/https://resizing.flixster.com/J-WXxwHSdv6w7VHxcCrPdebOvUA=/ems.cHJkLWVtcy1hc3NldHMvdHZzZWFzb24vNmE4MmUxOWItNGY5Yy00YzY0LTk5ODktZDY1MGEzZjdmZDFhLmpwZw==","imageLoading":""},{"aspectRatio":"ASPECT_RATIO_4_3","height":"1080","width":"1440","imageUrl":"https://resizing.flixster.com/N68GZ9kea8OaNmRYUvnhmnoGyz4=/fit-in/705x460/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_b_h9_aa.jpg","imageUrlMobile":"https://resizing.flixster.com/DRQvcw1LVxWuAdBlV0VnBC4biVg=/fit-in/352x330/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_b_h9_aa.jpg","imageLoading":""},{"aspectRatio":"ASPECT_RATIO_3_4","height":"2048","width":"1536","imageUrl":"https://resizing.flixster.com/Ck_we47bpcheYGfm4DspUvqCjXA=/fit-in/705x460/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_b_v10_aa.jpg","imageUrlMobile":"https://resizing.flixster.com/OqHcKlrfhRfnfjOd6-de39um0Pg=/fit-in/352x330/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_b_v10_aa.jpg","imageLoading":""},{"aspectRatio":"ASPECT_RATIO_16_9","height":"2160","width":"3840","imageUrl":"https://resizing.flixster.com/A6W_i7si2yuFTF58CVundvm0IWs=/fit-in/705x460/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_b_h8_aa.jpg","imageUrlMobile":"https://resizing.flixster.com/B14xZt1JRPCgEqEA4fhGzfIhf0g=/fit-in/352x330/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_b_h8_aa.jpg","imageLoading":""},{"aspectRatio":"ASPECT_RATIO_4_3","caption":"Peacemaker","height":"1080","width":"1440","imageUrl":"https://resizing.flixster.com/31A9AIs1ehVt4dSwvr3oIq1P7J0=/fit-in/705x460/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_i_h9_aa.jpg","imageUrlMobile":"https://resizing.flixster.com/7XyiXhn9BvpEhslyQpBn9hnXFr8=/fit-in/352x330/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_i_h9_aa.jpg","imageLoading":""},{"aspectRatio":"ASPECT_RATIO_3_4","caption":"Peacemaker","height":"1440","width":"1080","imageUrl":"https://resizing.flixster.com/U46-rgA8JKxNatvWQXOk2-75az0=/fit-in/705x460/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_i_v9_aa.jpg","imageUrlMobile":"https://resizing.flixster.com/BUCSSlBKkuZW7VLv3TBVqaOQ1zg=/fit-in/352x330/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_i_v9_aa.jpg","imageLoading":"lazy"},{"aspectRatio":"ASPECT_RATIO_2_3","caption":"Peacemaker","height":"1440","width":"960","imageUrl":"https://resizing.flixster.com/3bicZDYLXsHZV-OzWHo6IVenCc4=/fit-in/705x460/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_i_v8_aa.jpg","imageUrlMobile":"https://resizing.flixster.com/JiJ_5M6yk4RwL_eH8tnQn9m39tI=/fit-in/352x330/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_i_v8_aa.jpg","imageLoading":"lazy"},{"aspectRatio":"ASPECT_RATIO_16_9","caption":"Peacemaker","height":"1080","width":"1920","imageUrl":"https://resizing.flixster.com/vm58l9EGV6aj90uTvHW8usYGsrA=/fit-in/705x460/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_i_h10_aa.jpg","imageUrlMobile":"https://resizing.flixster.com/0twCfBybv6oYrFbstg0CuOgDgew=/fit-in/352x330/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/p30317930_i_h10_aa.jpg","imageLoading":"lazy"}],"picturesPageUrl":"/tv/peacemaker_2022/s02/pictures"} </script></photos-carousel-manager></section></div><ad-unit hidden unit-display="mobile" unit-type="mboxadtwo" show-ad-link><div slot="ad-inject" class="rectangle_ad mobile center"></div></ad-unit><ad-unit hidden unit-display="desktop" unit-type="opbannertwo"><div slot="ad-inject" class="banner-ad"></div></ad-unit><div class="modules-layout" tabindex="-1" data-ModulesNavigationManager="content:focusin"><div id="media-info" class="dom-anchor" data-ModulesNavigationManager="domAnchor"></div><section aria-labelledby="media-info-label" class="media-info" data-adobe-id="media-info" data-qa="section:media-info"><div class="header-wrap"><h2 class="unset" id="media-info-label"><rt-text context="heading" size="1.25" style="--textTransform: capitalize;" data-qa="title">Season Info </rt-text></h2></div><div class="content-wrap"><dl><div class="category-wrap" data-qa="item"><dt class="key"><rt-text class="key" size="0.875" data-qa="item-label">Executive Producer</rt-text></dt><dd data-qa="item-value-group"><rt-link href="/celebrity/james_gunn" data-qa="item-value">James Gunn</rt-link><rt-text class="delimiter">, </rt-text><rt-link href="/celebrity/peter_safran" data-qa="item-value">Peter Safran</rt-link><rt-text class="delimiter">, </rt-text><rt-link href="/celebrity/john_cena" data-qa="item-value">John Cena</rt-link></dd></div><div class="category-wrap" data-qa="item"><dt class="key"><rt-text class="key" size="0.875" data-qa="item-label">Screenwriter</rt-text></dt><dd data-qa="item-value-group"><rt-link href="/celebrity/james_gunn" data-qa="item-value">James Gunn</rt-link></dd></div><div class="category-wrap" data-qa="item"><dt class="key"><rt-text class="key" size="0.875" data-qa="item-label">Network</rt-text></dt><dd data-qa="item-value-group"><rt-text data-qa="item-value">HBO Max</rt-text></dd></div><div class="category-wrap" data-qa="item"><dt class="key"><rt-text class="key" size="0.875" data-qa="item-label">Rating</rt-text></dt><dd data-qa="item-value-group"><rt-text data-qa="item-value">TV-MA</rt-text></dd></div><div class="category-wrap" data-qa="item"><dt class="key"><rt-text class="key" size="0.875" data-qa="item-label">Genre</rt-text></dt><dd data-qa="item-value-group"><rt-link href="/browse/tv_series_browse/genres:comedy" data-qa="item-value">Comedy</rt-link><rt-text class="delimiter">, </rt-text><rt-link href="/browse/tv_series_browse/genres:action" data-qa="item-value">Action</rt-link></dd></div><div class="category-wrap" data-qa="item"><dt class="key"><rt-text class="key" size="0.875" data-qa="item-label">Original Language</rt-text></dt><dd data-qa="item-value-group"><rt-text data-qa="item-value">English</rt-text></dd></div><div class="category-wrap" data-qa="item"><dt class="key"><rt-text class="key" size="0.875" data-qa="item-label">Release Date</rt-text></dt><dd data-qa="item-value-group"><rt-text data-qa="item-value">Aug 21, 2025</rt-text></dd></div></dl></div></section></div></div><div id="sidebar-wrap"><div data-adobe-id="discovery-sidebar" data-DiscoverySidebarManager="sticky"><discovery-sidebar-manager><script data-json="discoverySidebarJSON" type="application/json">{"mediaType":"tvSeason"}</script></discovery-sidebar-manager><discovery-sidebar skeleton="panel" data-DiscoverySidebarManager="sidebar"></discovery-sidebar><ad-unit data-DiscoverySidebarManager="ad:instantiated" unit-display="desktop" unit-type="topmulti" show-ad-link><div slot="ad-inject"></div></ad-unit></div></div><script id="curation-json" type="application/json">{"emsId":"76add34d-b98d-34b5-9e0f-2eac74d2ab10","emsIdTvss":"c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed","rtId":"9072906","rtIdTvsn":"9072906","rtIdTvss":"16913","tvSeriesEmsId":"c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed","type":"tvSeason"}</script></div></div><overlay-base data-MediaAudienceReviewsManager="overlay" hidden><div slot="content"><media-review-full-audience><rt-button data-MediaAudienceReviewsManager="overlayClose:click" size="1" slot="close" theme="transparent"><rt-icon icon="close"></rt-icon></rt-button></media-review-full-audience></div></overlay-base><tool-tip data-MediaScorecardManager="tipCritics" hidden><rt-button slot="btnClose" data-MediaScorecardManager="tipCriticsClose:click" theme="transparent" size="1.5"><rt-icon icon="close" image="true"></rt-icon></rt-button><div data-MediaScorecardManager="tipCriticsContent"></div></tool-tip><tool-tip class="component" data-MediaScorecardManager="tipAudience" hidden><rt-button slot="btnClose" data-MediaScorecardManager="tipAudienceClose:click" theme="transparent" size="1.5"><rt-icon icon="close" image="true"></rt-icon></rt-button><div data-MediaScorecardManager="tipAudienceContent"></div></tool-tip><overlay-base data-MediaScorecardManager="overlay:close" hidden><div slot="content"></div></overlay-base><overlay-base data-JwPlayerManager="overlayBase:close" data-VideoPlayerOverlayManager="overlayBase:close,open" hidden><video-player-overlay class="video-overlay-wrap" data-qa="video-overlay" data-VideoPlayerOverlayManager="videoPlayerOverlay:unmute" slot="content"><rt-button data-JwPlayerManager="unmuteBtn:click" slot="unmuteBtn" theme="light"><rt-icon icon="volume-mute-fill"></rt-icon>&ensp; Tap to Unmute </rt-button><div slot="header"><button class="unset transparent" data-VideoPlayerOverlayManager="btnOverlayClose:click" data-qa="video-close-btn"><rt-icon icon="close"><span class="sr-only">Close video</span></rt-icon></button><a class="cta-btn header-cta button hide">See Details</a></div><div slot="content"></div><a slot="footer" class="cta-btn footer-cta button hide">See Details</a></video-player-overlay></overlay-base><div id="video-overlay-player" hidden></div><video-player-overlay-manager></video-player-overlay-manager><jw-player-manager data-AdsVideoSpotlightManager="jwPlayerManager:playlistItem,ready,remove" data-VideoPlayerOverlayManager="jwPlayerManager:playlistItem,pause,ready,relatedClose,relatedOpen"></jw-player-manager><overlay-base data-PhotosCarouselManager="overlayBase:close" hidden><photos-carousel-overlay data-PhotosCarouselManager="photosOverlay:sliderBtnClick" slot="content"><rt-button data-PhotosCarouselManager="closeBtn:click" slot="closeBtn" theme="transparent"><rt-icon icon="close"></rt-icon></rt-button></photos-carousel-overlay></overlay-base><overlay-base data-RateAndReviewOverlayManager="overlayBase:close" hidden noclickoutside><div slot="content"></div></overlay-base><toast-notification data-RateAndReviewOverlayManager="toast" aria-live="polite" hidden><rt-icon slot="icon" icon="check-circled" image size="1"></rt-icon><rt-text slot="message" data-RateAndReviewOverlayManager="toastMessage" context="label" size="0.875">- -</rt-text><rt-button slot="close" theme="transparent"><rt-icon icon="close" image size="1"></rt-icon></rt-button></toast-notification><ads-media-scorecard-manager></ads-media-scorecard-manager></div><back-to-top hidden></back-to-top></main><ad-unit hidden unit-display="desktop" unit-type="bottombanner"><div slot="ad-inject" class="sleaderboard_wrapper"></div></ad-unit><ads-global-skin-takeover-manager></ads-global-skin-takeover-manager><footer-manager></footer-manager><footer class="footer container" data-PagePicturesManager="footer"><mobile-app-desktop-footer env="production" hidden></mobile-app-desktop-footer><div class="footer__content-desktop-block" data-qa="footer:section"><div class="footer__content-group"><ul class="footer__links-list"><li class="footer__links-list-item"><a href="/help_desk" data-qa="footer:link-helpdesk">Help</a></li><li class="footer__links-list-item"><a href="/about" data-qa="footer:link-about">About Rotten Tomatoes</a></li><li id="footer-feedback" class="footer__links-list-item" data-qa="footer-feedback-desktop"></li></ul></div><div class="footer__content-group"><ul class="footer__links-list"><li class="footer__links-list-item"><a href="/critics/criteria" data-qa="footer:link-critic-submission">Critic Submission</a></li><li class="footer__links-list-item"><a href="/help_desk/licensing" data-qa="footer:link-licensing">Licensing</a></li><li class="footer__links-list-item"><a href="https://together.nbcuni.com/advertise/?utm_source=rotten_tomatoes&amp;utm_medium=referral&amp;utm_campaign=property_ad_pages&amp;utm_content=footer" target="_blank" rel="noopener" data-qa="footer:link-ads">Advertise With Us</a></li><li class="footer__links-list-item"><a href="//www.fandango.com/careers" target="_blank" rel="noopener" data-qa="footer:link-careers">Careers</a></li></ul></div><div class="footer__content-group footer__newsletter-block"><p class="h3 footer__content-group-title"><rt-icon icon="mail" size="1.25" style="fill:#fff"></rt-icon>&ensp;Join the Newsletter </p><p class="footer__newsletter-copy">Get the freshest reviews, news, and more delivered right to your inbox!</p><rt-button shape="pill" data-FooterManager="btnNewsLetter:click" data-qa="footer-newsletter-desktop">Join The Newsletter </rt-button><a data-FooterManager="linkNewsLetter" class="button footer__newsletter-btn hide" target="_blank" rel="noopener">Join The Newsletter </a></div><div class="footer__content-group footer__social-block" data-qa="footer:social"><p class="h3 footer__content-group-title">Follow Us</p><social-media-icons theme="light" size="20"></social-media-icons></div></div><div class="footer__content-mobile-block" data-qa="mfooter:section"><div class="footer__content-group"><div class="mobile-app-cta-wrap"><mobile-app-cta env="production" showandroid="false" showios="true" hidden></mobile-app-cta></div><p class="footer__copyright-legal" data-qa="mfooter:copyright"><rt-text size="0.75">Copyright &copy; Fandango. All rights reserved.</rt-text></p><p><rt-button shape="pill" data-FooterManager="btnNewsLetter:click" data-qa="footer-newsletter-mobile">Join The Newsletter</rt-button></p><a data-FooterManager="linkNewsLetter" class="button footer__newsletter-btn hide" target="_blank" rel="noopener">Join The Newsletter</a><ul class="footer__links-list list-inline"><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" rel="noopener" data-qa="mfooter:link-privacy-policy">Privacy Policy </a></li><li class="footer__links-list-item"><a href="/policies/terms-and-policies" data-qa="mfooter:link-terms-policies">Terms and Policies</a></li><li class="footer__links-list-item"><img data-FooterManager="iconCCPA" src="https://images.fandango.com/cms/assets/266533e0-7afb-11ed-83f2-4f600722b564--privacyoptions.svg" class="footer__ccpa-icon" loading="lazy" alt="CCPA icon" /><a href="javascript:void(0)" id="ot-sdk-btn" class="ot-sdk-show-settings mobile" data-qa="footer-cookie-settings-mobile">Cookie Settings</a></li><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/privacy/california-consumer-privacy-act" target="_blank" rel="noopener" data-qa="mfooter:link-california-notice">California Notice</a></li><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/privacy/cookies#accordionheader2" target="_blank" rel="noopener" data-qa="mfooter:link-adChoices">Ad Choices</a></li><li id="footer-feedback-mobile" class="footer__links-list-item" data-qa="footer-feedback-mobile"></li><li class="footer__links-list-item"><a href="/faq#accessibility" data-qa="mfooter:link-accessibility">Accessibility</a></li></ul></div></div><div class="footer__copyright"><ul class="footer__links-list list-inline list-inline--separator" data-qa="footer:links-list-privacy"><li class="footer__links-list-item version" data-qa="footer:version"><span>V3.1</span></li><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/fandango-privacy-policy" target="_blank" rel="noopener" data-qa="footer:link-privacy-policy">Privacy Policy </a></li><li class="footer__links-list-item"><a href="/policies/terms-and-policies" data-qa="footer:link-terms-policies">Terms and Policies</a></li><li class="footer__links-list-item"><img data-FooterManager="iconCCPA" src="https://images.fandango.com/cms/assets/266533e0-7afb-11ed-83f2-4f600722b564--privacyoptions.svg" class="footer__ccpa-icon" loading="lazy" alt="CCPA icon" /><a href="javascript:void(0)" id="ot-sdk-btn" class="ot-sdk-show-settings" data-qa="footer-cookie-settings-desktop">Cookie Settings</a></li><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/privacy/california-consumer-privacy-act" target="_blank" rel="noopener" data-qa="footer:link-california-notice">California Notice</a></li><li class="footer__links-list-item"><a href="https://www.nbcuniversalprivacy.com/privacy/cookies#accordionheader2" target="_blank" rel="noopener" data-qa="footer:link-adChoices">Ad Choices</a></li><li class="footer__links-list-item"><a href="/faq#accessibility" data-qa="footer:link-accessibility">Accessibility</a></li></ul><span class="footer__copyright-legal" data-qa="footer:copyright">Copyright &copy; Fandango. A Division of <a href="https://www.nbcuniversal.com" target="_blank" rel="noopener" data-qa="footer:link-nbcuniversal">NBCUniversal</a>. All rights reserved. </span></div></footer></div><iframe-container hidden data-ArtiManager="iframeContainer:close,resize" data-iframe-src="https://arti.rottentomatoes.com?theme=iframe" theme="widget"><span slot="logo"><img src="/assets/pizza-pie/images/arti.041d204c4a4.svg" alt="Logo"></img><span>beta</span></span><rt-button aria-label="New chat" data-ArtiManager="btnNewChat:click" id="artiNewChatButton" slot="optBtns" theme="transparent" title="New chat"><rt-icon icon="new-chat" size="1.25" image></rt-icon></rt-button></iframe-container><arti-manager></arti-manager><script type="text/javascript">(function (root){ root.RottenTomatoes || (root.RottenTomatoes={}); root.RottenTomatoes.context || (root.RottenTomatoes.context={}); root.RottenTomatoes.context.resetCookies=["AMCVS_8CF467C25245AE3F0A490D4C%40AdobeOrg", "AMCV_8CF467C25245AE3F0A490D4C%40AdobeOrg", "WRIgnore", "WRUIDAWS", "__CT_Data", "__gads", "_admrla", "_awl", "_cs_c", "_cs_id", "_cs_mk", "_cs_s", "_fbp", "_ga", "_gat_gtmTracker", "_gid", "aam_uuid", "akamai_generated_location", "auth_token", "auth_user", "auth_client", "check", "cognito", "fblo_326803741017", "fbm_326803741017", "fbsr_326803741017", "gpv_Page", "id_token", "is_auth", "loginPlatform", "mbox", "notice_behavior", "optimizelyBuckets", "optimizelyEndUserId", "optimizelyPendingLogEvents", "optimizelySegments", "s_cc", "s_dayslastvisit", "s_dayslastvisit_s", "s_invisit", "s_prevPage", "s_sq", "s_vnum", "cognito", "fbm_326803741017", "fbsr_326803741017", "id_token", "JSESSIONID", "QSI_HistorySession", "QSI_SI_8up4dWDOtjAg0hn_intercept", "_ALGOLIA", "__Host-color-scheme", "__Host-theme-options", "__host_color_scheme", "__host_theme_options", "_cb", "_cb_ls", "_cb_svref", "_chartbeat2", "_chartbeat4", "_chartbeat5", "_sp_id.47f3", "_sp_ses.47f3", "_v__chartbeat3", "adops_master_kvs", "akacd_RTReplatform", "algoliaUT", "cognito", "cl_duid", "fbsr_326803741017", "id_token", "mps_uuid", "session_id", "_admrla", "_awl", "_ga", "_gid", "aam_uuid", "cognito", "fbm_326803741017", "id_token", "_cb", "_cb_ls", "_cb_svref", "_chartbeat2", "adops_master_kvs", "cognito", "id_token", "krg_crb", "krg_uid", "mps_uuid"]; root.Fandango || (root.Fandango={}); root.Fandango.dtmData={ "webVersion": "node", "rtVersion": 3.1, "loggedInStatus": "", "customerId": "", "pageName": "trailers"}; root.RottenTomatoes.context.video={ "file": "https:\u002F\u002Flink.theplatform.com\u002Fs\u002FNGweTC\u002Fmedia\u002F2pzVnJjkcs18?formats=MPEG-DASH+widevine,M3U+appleHlsEncryption,M3U+none,MPEG-DASH+none,MPEG4,MP3", "type": "hls", "description": "Doris (Lulu Wilson) subtly hints at her plans for Mikey (Parker Mack).", "image": "https:\u002F\u002Fstatcdn.fandango.com\u002FMPX\u002Fimage\u002FNBCU_Fandango\u002F300\u002F146\u002Fthumb_04E6645A-8AA7-42B4-8F0B-64F1F55FEF08.jpg", "isRedBand": false, "mediaid": "1013925955554", "mpxId": "1013925955554", "publicId": "2pzVnJjkcs18", "title": "Ouija: Origin of Evil: Official Clip - Creepy Little Sister", "default": false, "label": "0", "duration": "1:50", "durationInSeconds": "110.027", "emsMediaType": "Movie", "emsId": "6d73f951-119d-3a36-8eba-ab319447e477", "overviewPageUrl": "\u002Fm\u002Fouija_origin_of_evil", "videoPageUrl": "\u002Fm\u002Fouija_origin_of_evil\u002Fvideos\u002F2pzVnJjkcs18", "videoType": "CLIP", "adobeDataLayer":{ "content":{ "id": "fandango_1013925955554", "length": "110.027", "type": "vod", "player_name": "jw", "sdk_version": "web: 6.51.0", "channel": "movie", "originator": "universal pictures", "name": "ouija: origin of evil: official clip - creepy little sister", "rating": "not adult", "stream_type": "video"}, "media_params":{ "genre": "horror, mystery & thriller", "show_type": 2}}, "comscore":{ "labelmapping": "c3=\"rottentomatoes.com\", ns_st_st=\"Rotten Tomatoes\", ns_st_pu=\"Universal Pictures\", ns_st_pr=\"Ouija: Origin of Evil\", ns_st_sn=\"*null\", ns_st_en=\"*null\", ns_st_ge=\"Horror,Mystery & Thriller\", ns_st_ia=\"0\", ns_st_ce=\"0\", ns_st_ddt=\"2016\", ns_st_tdt=\"2016\""}, "thumbnail": "https:\u002F\u002Fresizing.flixster.com\u002FerVt9qj9-5hR_PQXuQCOJtouGW4=\u002F270x160\u002Fv2\u002Fhttps:\u002F\u002Fstatcdn.fandango.com\u002FMPX\u002Fimage\u002FNBCU_Fandango\u002F300\u002F146\u002Fthumb_04E6645A-8AA7-42B4-8F0B-64F1F55FEF08.jpg"}; root.RottenTomatoes.context.videoClipsJson={ "count": 12}; root.RottenTomatoes.criticPage={ "vanity": "henry-goldblatt", "type": "tv", "typeDisplayName": "TV", "totalReviews": "", "criticID": "15036"}; root.RottenTomatoes.context.review={ "mediaType": "movie", "title": "A Few Days of Respite", "emsId": "c2c97ea5-fd07-3901-a487-778c022eb4f2", "type": "all", "sort": undefined, "reviewsCount": 0, "pageInfo": undefined, "reviewerDefaultImg": "https:\u002F\u002Fimages.fandango.com\u002Fcms\u002Fassets\u002F5b6ff500-1663-11ec-ae31-05a670d2d590--rtactordefault.png", "reviewerDefaultImgWidth": "100"}; root.RottenTomatoes.context.useCursorPagination=true; root.RottenTomatoes.context.verifiedTooltip=undefined; root.RottenTomatoes.context.layout={ "header":{ "movies":{ "moviesAtHome":{ "tarsSlug": "rt-nav-movies-at-home", "linkList": [{ "header": "Fandango at Home", "slug": "fandango-at-home-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Faffiliates:fandango-at-home"},{ "header": "Peacock", "slug": "peacock-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Faffiliates:peacock"},{ "header": "Netflix", "slug": "netflix-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Faffiliates:netflix"},{ "header": "Apple TV+", "slug": "apple-tv-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Faffiliates:apple-tv-plus"},{ "header": "Prime Video", "slug": "prime-video-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Faffiliates:prime-video"},{ "header": "Most Popular Streaming movies", "slug": "most-popular-streaming-movies-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Fsort:popular"},{ "header": "Certified Fresh movies", "slug": "certified-fresh-movies-link", "url": "\u002Fbrowse\u002Fmovies_at_home\u002Fcritics:certified_fresh"},{ "header": "Browse all", "slug": "browse-all-link", "url": "\u002Fbrowse\u002Fmovies_at_home"}]}}, "editorial":{ "guides":{ "posts": [{ "ID": 161109, "author": 12, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2023\u002F09\u002F600EssentialFootballMovies.png"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fguide\u002Fbest-football-movies\u002F", "status": "publish", "title": "59 Best Football Movies, Ranked by Tomatometer", "type": "guide"},{ "ID": 253470, "author": 12, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2025\u002F08\u002FBest_New_Romcoms600.jpg"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fguide\u002Fbest-new-rom-coms-romance-movies\u002F", "status": "publish", "title": "50 Best New Rom-Coms and Romance Movies", "type": "guide"}], "title": "Guides", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fcountdown\u002F"}, "hubs":{ "posts": [{ "ID": 237626, "author": 12, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2023\u002F05\u002FRT_WTW_Generic_2023_Thumbnail_600x314_021623.jpg"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Frt-hub\u002Fwhat-to-watch\u002F", "status": "publish", "title": "What to Watch: In Theaters and On Streaming", "type": "rt-hub"},{ "ID": 140214, "author": 12, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2023\u002F02\u002FRT_AwardsTour_Thumbnail_600x314.jpg"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Frt-hub\u002Fawards-tour\u002F", "status": "publish", "title": "Awards Tour", "type": "rt-hub"}], "title": "Hubs", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Frt-hubs\u002F"}, "news":{ "posts": [{ "ID": 273082, "author": 79, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2025\u002F08\u002FNew_Streaming_September_2025-Rep.jpg"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002Fnew-movies-and-shows-streaming-in-september-2025-what-to-watch-on-netflix-prime-video-hbo-max-disney-and-more\u002F", "status": "publish", "title": "New Movies and Shows Streaming in September: What to watch on Netflix, Prime Video, HBO Max, Disney+ and More", "type": "article"},{ "ID": 273326, "author": 669, "featured_image":{ "source": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwp-content\u002Fuploads\u002F2025\u002F09\u002FConjuring_Last_Rites_Reviews-Rep.jpg"}, "link": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002Fthe-conjuring-last-rites-first-reviews\u002F", "status": "publish", "title": "\u003Cem\u003EThe Conjuring: Last Rites\u003C\u002Fem\u003E First Reviews: A Frightful, Fitting Send-off", "type": "article"}], "title": "RT News", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fnews\u002F"}}, "trendingTarsSlug": "rt-nav-trending", "trending": [{ "header": "Emmy Noms", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002F2025-emmys-ballot-complete-with-tomatometer-and-popcornmeter-scores\u002F"},{ "header": "Re-Release Calendar", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002Fmovie-re-releases-calendar\u002F"},{ "header": "Renewed and Cancelled TV", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002Frenewed-and-cancelled-tv-shows-2025\u002F"},{ "header": "The Rotten Tomatoes App ", "url": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Farticle\u002Fapp\u002F"}], "certifiedMedia":{ "certifiedFreshTvSeason":{ "header": null, "media":{ "url": "\u002Ftv\u002Fthe_paper_2025\u002Fs01", "name": "The Paper: Season 1", "score": 83, "posterImg": "https:\u002F\u002Fresizing.flixster.com\u002FyFijQcjPYUWUelgmiZLHgkXU7hw=\u002F206x305\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FDFkkHf5pEVX_apKtIQZcoEvI6RU=\u002Ffit-in\u002F180x240\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FtexEZJLAG-KcVpfCdkT2R1t4cmE=\u002Fems.cHJkLWVtcy1hc3NldHMvdHZzZWFzb24vYTM3OWM2MTctN2M3Ny00MjdhLTk4NDUtODE5ZWUwMWExNGRhLnBuZw=="}, "tarsSlug": "rt-nav-list-cf-picks"}, "certifiedFreshMovieInTheater":{ "header": null, "media":{ "url": "\u002Fm\u002Ftwinless", "name": "Twinless", "score": 98, "posterImg": "https:\u002F\u002Fresizing.flixster.com\u002Fj7lw2KeY9_XyfZQdqRZGku7_9C8=\u002F206x305\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FuxoeWz7uWmeYIV94_SzEV_osqe4=\u002Ffit-in\u002F180x240\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FVlylB3xT2RIYmRivMx37O3yD76Q=\u002Fems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2ZlNDQ1MGQ5LTFjN2QtNDIwNC04NWE1LTM5NGM4N2U5ZTgzYy5qcGc="}}, "certifiedFreshMovieInTheater4":{ "header": null, "media":{ "url": "\u002Fm\u002Fhamilton_2020", "name": "Hamilton", "score": 98, "posterImg": "https:\u002F\u002Fresizing.flixster.com\u002F1woquJmQfEhWCZtm7GcH0NMHsYA=\u002F206x305\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FPeAJ5ZpF5qB98ZiX6ixNDCgW2P0=\u002Ffit-in\u002F180x240\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FVmBvlTk8-z7pQvDZXTgSdj93WDE=\u002Fems.cHJkLWVtcy1hc3NldHMvbW92aWVzLzkzY2IxZjFkLTE1NjEtNDQ4Yi05NDY3LTcxNzFmMDVhMDczNi5qcGc="}}, "certifiedFreshMovieAtHome":{ "header": null, "media":{ "url": "\u002Fm\u002Fthe_thursday_murder_club", "name": "The Thursday Murder Club", "score": 76, "posterImg": "https:\u002F\u002Fresizing.flixster.com\u002FjeeldFGcfSMgG09ey5VB7TCFiek=\u002F206x305\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002F9LXDkCzIBBNEiPURkB9t6VefF5Q=\u002Ffit-in\u002F180x240\u002Fv2\u002Fhttps:\u002F\u002Fresizing.flixster.com\u002FrwdeR5xIiN0k7SWr6yXdnmb6zP8=\u002Fems.cHJkLWVtcy1hc3NldHMvbW92aWVzL2EzYWFkZWJiLWE5N2MtNDc3MS1iMDRlLTk0YWVlYzI5M2UxZS5qcGc="}}, "tarsSlug": "rt-nav-list-cf-picks"}, "tvLists":{ "newTvTonight":{ "tarsSlug": "rt-hp-text-list-3", "title": "New TV Tonight", "shows": [{ "title": "Task: Season 1", "tomatometer":{ "tomatometer": 89, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Ftask\u002Fs01"},{ "title": "The Walking Dead: Daryl Dixon: Season 3", "tomatometer":{ "tomatometer": 80, "sentiment": "positive", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fthe_walking_dead_daryl_dixon\u002Fs03"},{ "title": "The Crow Girl: Season 1", "tomatometer":{ "tomatometer": 80, "sentiment": "positive", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fthe_crow_girl\u002Fs01"},{ "title": "Only Murders in the Building: Season 5", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fonly_murders_in_the_building\u002Fs05"},{ "title": "The Girlfriend: Season 1", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fthe_girlfriend\u002Fs01"},{ "title": "aka Charlie Sheen: Season 1", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Faka_charlie_sheen\u002Fs01"},{ "title": "Wizards Beyond Waverly Place: Season 2", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fwizards_beyond_waverly_place\u002Fs02"},{ "title": "Seen & Heard: the History of Black Television: Season 1", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fseen_and_heard_the_history_of_black_television\u002Fs01"},{ "title": "The Fragrant Flower Blooms With Dignity: Season 1", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fthe_fragrant_flower_blooms_with_dignity\u002Fs01"},{ "title": "Guts & Glory: Season 1", "tomatometer":{ "tomatometer": null, "sentiment": "empty", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fguts_and_glory\u002Fs01"}]}, "mostPopularTvOnRt":{ "tarsSlug": "rt-hp-text-list-2", "title": "Most Popular TV on RT", "shows": [{ "title": "The Paper: Season 1", "tomatometer":{ "tomatometer": 83, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fthe_paper_2025\u002Fs01"},{ "title": "Dexter: Resurrection: Season 1", "tomatometer":{ "tomatometer": 95, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fdexter_resurrection\u002Fs01"},{ "title": "Alien: Earth: Season 1", "tomatometer":{ "tomatometer": 95, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Falien_earth\u002Fs01"},{ "title": "Task: Season 1", "tomatometer":{ "tomatometer": 89, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Ftask\u002Fs01"},{ "title": "Wednesday: Season 2", "tomatometer":{ "tomatometer": 87, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fwednesday\u002Fs02"},{ "title": "Peacemaker: Season 2", "tomatometer":{ "tomatometer": 99, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fpeacemaker_2022\u002Fs02"},{ "title": "The Terminal List: Dark Wolf: Season 1", "tomatometer":{ "tomatometer": 73, "sentiment": "positive", "certified": false}, "tvPageUrl": "\u002Ftv\u002Fthe_terminal_list_dark_wolf\u002Fs01"},{ "title": "Hostage: Season 1", "tomatometer":{ "tomatometer": 82, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fhostage_2025\u002Fs01"},{ "title": "Chief of War: Season 1", "tomatometer":{ "tomatometer": 93, "sentiment": "positive", "certified": true}, "tvPageUrl": "\u002Ftv\u002Fchief_of_war\u002Fs01"},{ "title": "Irish Blood: Season 1", "tomatometer":{ "tomatometer": 100, "sentiment": "positive", "certified": false}, "tvPageUrl": "\u002Ftv\u002Firish_blood\u002Fs01"}]}}}, "links":{ "moviesInTheaters":{ "certifiedFresh": "\u002Fbrowse\u002Fmovies_in_theaters\u002Fcritics:certified_fresh~sort:popular", "comingSoon": "\u002Fbrowse\u002Fmovies_coming_soon\u002F", "openingThisWeek": "\u002Fbrowse\u002Fmovies_in_theaters\u002Fsort:newest", "title": "\u002Fbrowse\u002Fmovies_in_theaters", "topBoxOffice": "\u002Fbrowse\u002Fmovies_in_theaters"}, "onDvdAndStreaming":{ "all": "\u002Fbrowse\u002Fmovies_at_home\u002F", "certifiedFresh": "\u002Fbrowse\u002Fmovies_at_home\u002Fcritics:certified_fresh", "title": "\u002Fbrowse\u002Fmovies_at_home\u002F", "top": "\u002Fbrowse\u002Fmovies_at_home\u002Fsort:popular"}, "moreMovies":{ "topMovies": "\u002Fbrowse\u002Fmovies_at_home\u002Fsort:popular", "trailers": "\u002Ftrailers"}, "tvTonight": "\u002Fbrowse\u002Ftv_series_browse\u002Fsort:newest", "tvPopular": "\u002Fbrowse\u002Ftv_series_browse\u002Fsort:popular", "moreTv":{ "topTv": "\u002Fbrowse\u002Ftv_series_browse\u002Fsort:popular", "certifiedFresh": "\u002Fbrowse\u002Ftv_series_browse\u002Fcritics:fresh"}, "editorial":{ "allTimeLists": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fall-time-lists\u002F", "bingeGuide": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fbinge-guide\u002F", "comicsOnTv": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fcomics-on-tv\u002F", "countdown": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fcountdown\u002F", "fiveFavoriteFilms": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Ffive-favorite-films\u002F", "videoInterviews": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fvideo-interviews\u002F", "weekendBoxOffice": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fweekend-box-office\u002F", "weeklyKetchup": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fweekly-ketchup\u002F", "whatToWatch": "https:\u002F\u002Feditorial.rottentomatoes.com\u002Fwhat-to-watch\u002F"}, "advertisingFooter": "https:\u002F\u002Ftogether.nbcuni.com\u002Fadvertise\u002F?utm_source=rotten_tomatoes&utm_medium=referral&utm_campaign=property_ad_pages&utm_content=footer", "californiaNotice": "https:\u002F\u002Fwww.nbcuniversalprivacy.com\u002Fprivacy\u002Fcalifornia-consumer-privacy-act", "careers": "\u002F\u002Fwww.fandango.com\u002Fcareers", "cookieManagement": "https:\u002F\u002Fwww.nbcuniversalprivacy.com\u002Fprivacy\u002Fcookies#accordionheader2", "fandangoAbout": "https:\u002F\u002Fwww.fandango.com\u002Fabout-us", "privacyPolicy": "https:\u002F\u002Fwww.nbcuniversalprivacy.com\u002Ffandango-privacy-policy", "termsPolicies": "\u002Fpolicies\u002Fterms-and-policies"}}; root.RottenTomatoes.thirdParty={ "chartBeat":{ "auth": "64558", "domain": "rottentomatoes.com"}, "mpx":{ "accountPid": "NGweTC", "playerPid": "y__7B0iQTi4P", "playerPidPDK6": "pdk6_y__7B0iQTi4P", "accountId": "2474312077"}, "algoliaSearch":{ "aId": "79FRDP12PN", "sId": "175588f6e5f8319b27702e4cc4013561"}, "cognito":{ "upId": "us-west-2_4L0ZX4b1U", "clientId": "7pu48v8i2n25t4vhes0edck31c"}}; root.RottenTomatoes.serviceWorker={ "isServiceWokerOn": true}; root.__RT__ || (root.__RT__={}); root.__RT__.featureFlags={ "adsCarouselHP": false, "adsCarouselHPSlug": "rt-sponsored-carousel-list-mcdonalds-hp", "adsCarouselOP": false, "adsCarouselOPSlug": "rt-sponsored-carousel-list-mcdonalds-op", "adsMockDLP": false, "adsPages": "none", "adsSponsoredOverrideOP": true, "adsSponsoredOverrideOPSlugs": "rt-sponsored-override-op-starz", "adsVideoSpotlightHP": false, "appleSigninEnabled": true, "artiEnabled": true, "authPasswordEnabled": true, "authVerboseLogs": false, "bypassCriticValidationEnabled": false, "castAndCrewEnabled": true, "cookieConsentServiceEnabled": false, "crssoEnabled": false, "editorialApiDisabled": false, "faqUpdatesEnabled": true, "legacyBridge": true, "logVerboseEnabled": false, "mobileAppAndroid": "https:\u002F\u002Fplay.google.com\u002Fstore\u002Fapps\u002Fdetails?id=com.rottentomatoes.android", "mobileAppIos": "https:\u002F\u002Fapps.apple.com\u002Fus\u002Fapp\u002Frotten-tomatoes-movies-tv\u002Fid6673916573", "mobileAppIosMeta": "app-id=6673916573, app-argument=https:\u002F\u002Fwww.rottentomatoes.com\u002F", "mobileNavEnabled": true, "oneTrustJwtApiUrl": "https:\u002F\u002Fonetrustjwt.services.fandango.com", "oneTrustJwtServiceEnabled": false, "pageJsonEnabled": false, "profilesFeaturesEnabled": false, "profilesUsernameEnabled": false, "redesignMediaHeroEnabled": true, "redesignMoreLikeThis": true, "redesignSortTable": true, "trafficAndroidEnabled": false, "trafficSafariEnabled": true, "userMigrationEnabled": true, "versantFreewheelEnabled": false, "versantMpsDomain": "app.mps.vsnt.net", "versantMpsEnabled": false, "versantOneTrustScriptBlock": "\u003C!-- OneTrust Cookies Consent Notice start for rottentomatoes.com --\u003E \u003Cscript src=\"https:\u002F\u002Fcdn.cookielaw.org\u002Fconsent\u002F01978557-1604-76a7-ad7c-18216757cf52-test\u002FotSDKStub.js\" type=\"text\u002Fjavascript\" charset=\"UTF-8\" data-domain-script=\"01978557-1604-76a7-ad7c-18216757cf52-test\" integrity=\"sha384-Exfxdyaw5OnsUlHEKlNlz7OwgVCyLlitAtJsDmSNh3LeLlCjWXos3X\u002FCMNUbQ\u002FgA\" crossorigin=\"anonymous\" \u003E\u003C\u002Fscript\u003E \u003Cscript type=\"text\u002Fjavascript\"\u003E function OptanonWrapper(){ if (OnetrustActiveGroups.includes('7')){ document.querySelector('search-results-nav-manager')?.setAlgoliaInsightUserToken?.();}} \u003C\u002Fscript\u003E \u003C!-- OneTrust Cookies Consent Notice end for rottentomatoes.com --\u003E \u003C!-- OneTrust IAB US Privacy (USP) --\u003E \u003Cscript src=\"https:\u002F\u002Fcdn.cookielaw.org\u002Fopt-out\u002FotCCPAiab.js\" id=\"privacyCookie\" type=\"text\u002Fjavascript\" charset=\"UTF-8\" ccpa-opt-out-ids=\"USP\" ccpa-opt-out-geo=\"US\" ccpa-opt-out-lspa=\"false\"\u003E\u003C\u002Fscript\u003E \u003C!-- OneTrust IAB US Privacy (USP) end --\u003E", "videoGeoFencingEnabled": true}; root.RottenTomatoes.context.adsMockDLP=false; root.RottenTomatoes.context.req={ "params":{ "vanity": "peacemaker_2022", "tvSeason": "s02"}, "query":{}, "route":{}, "url": "\u002Ftv\u002Fpeacemaker_2022\u002Fs02", "secure": false, "buildVersion": undefined}; root.RottenTomatoes.context.config={}; root.BK={ "PageName": "http:\u002F\u002Fwww.rottentomatoes.com\u002Ftv\u002Fpeacemaker_2022\u002Fs02", "SiteID": 37528, "SiteSection": "tv", "TvSeriesTitle": "Peacemaker", "TvSeriesId": "c06d0b8a-9ae4-33f6-beb0-d29cb62dd7ed"}; root.RottenTomatoes.dtmData={ "customerId": "", "loggedInStatus": "", "rtVersion": 3.1, "webVersion": "node", "emsID": "76add34d-b98d-34b5-9e0f-2eac74d2ab10", "Season Title": "Season 2", "Series Title": "Peacemaker", "pageName": "rt | tv | season | Peacemaker | Season 2", "titleGenre": "Comedy", "titleId": "76add34d-b98d-34b5-9e0f-2eac74d2ab10", "titleName": "Peacemaker", "titleType": "Tv"}; root.RottenTomatoes.context.gptSite="tv";}(this)); </script><script fetchpriority="high" src="/assets/pizza-pie/javascripts/bundles/roma/preload.18bcfff8e54.js"></script><script src="/assets/pizza-pie/javascripts/bundles/roma/vendors.a4cc402b78a.js"></script><script src="/assets/pizza-pie/javascripts/bundles/roma/default.24dc1977289.js"></script><script async data-SearchResultsNavManager="script:load" src="https://cdn.jsdelivr.net/npm/algoliasearch@4/dist/algoliasearch-lite.umd.js"></script><script src="/assets/pizza-pie/javascripts/templates/roma/searchNav.a3288ea5efe.js"></script><script src="/assets/pizza-pie/javascripts/bundles/roma/searchNav.6a836b4ca81.js"></script><script src="/assets/pizza-pie/javascripts/templates/pages/tvSeason/index.3aa173d4c0f.js"></script><script src="/assets/pizza-pie/javascripts/bundles/pages/tvSeason/index.bd374ef0595.js"></script><script>if (window.mps && typeof window.mps.writeFooter==='function'){ window.mps.writeFooter();} </script><script>window._satellite && _satellite.pageBottom(); </script></body></html> 1 + <!DOCTYPE html><html lang="en" dir="ltr" xmlns="http://www.w3.org/1999/xhtml" prefix="fb: http://www.facebook.com/2008/fbml og: http://opengraphprotocol.org/schema/"><head prefix="og: http://ogp.me/ns# flixstertomatoes: http://ogp.me/ns/apps/flixstertomatoes#"><script charset="UTF-8" crossorigin="anonymous" data-domain-script="7e979733-6841-4fce-9182-515fac69187f" integrity="sha384-TKdmlzVmoD70HzftTw4WtOzIBL5mNx8mXSRzEvwrWjpIJ7FZ/EuX758yMDWXtRUN" src="https://cdn.cookielaw.org/consent/7e979733-6841-4fce-9182-515fac69187f/otSDKStub.js" type="text/javascript"></script><script type="text/javascript">function OptanonWrapper(){ if (OnetrustActiveGroups.includes('7')){ document.querySelector('search-results-nav-manager')?.setAlgoliaInsightUserToken?.()}} </script><script ccpa-opt-out-ids="USP" ccpa-opt-out-geo="US" ccpa-opt-out-lspa="false" charset="UTF-8" src="https://cdn.cookielaw.org/opt-out/otCCPAiab.js" type="text/javascript"></script><script src="/assets/pizza-pie/javascripts/bundles/roma/rt-common.js?single"></script><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><meta http-equiv="x-ua-compatible" content="ie=edge"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="shortcut icon" sizes="76x76" type="image/x-icon" href="https://www.rottentomatoes.com/assets/pizza-pie/images/favicon.ico" /><title>Peacemaker: Season 2 | Rotten Tomatoes</title><meta name="description" content="Discover reviews, ratings, and trailers for Peacemaker: Season 2 on Rotten Tomatoes. Stay updated with critic and audience scores today!" /><meta name="twitter:card" content="summary" /><meta name="twitter:image" content="https://resizing.flixster.com/icqaMuFdXKqmN8qur-dfG4I-hWs=/206x305/v2/https://resizing.flixster.com/J-WXxwHSdv6w7VHxcCrPdebOvUA=/ems.cHJkLWVtcy1hc3NldHMvdHZzZWFzb24vNmE4MmUxOWItNGY5Yy00YzY0LTk5ODktZDY1MGEzZjdmZDFhLmpwZw==" /><meta name="twitter:title" content="Peacemaker: Season 2 | Rotten Tomatoes" /><meta name="twitter:text:title" content="Peacemaker: Season 2 | Rotten Tomatoes" /><meta name="twitter:description" content="Discover reviews, ratings, and trailers for Peacemaker: Season 2 on Rotten Tomatoes. Stay updated with critic and audience scores today!" /><meta property="og:site_name" content="Rotten Tomatoes" /><meta property="og:title" content="Peacemaker: Season 2 | Rotten Tomatoes" /><meta property="og:description" content="Discover reviews, ratings, and trailers for Peacemaker: Season 2 on Rotten Tomatoes. Stay updated with critic and audience scores today!" /><meta property="og:type" content="video.tv_show" /><meta property="og:url" content="https://www.rottentomatoes.com/tv/peacemaker_2022/s02" /><meta property="og:image" content="https://resizing.flixster.com/icqaMuFdXKqmN8qur-dfG4I-hWs=/206x305/v2/https://resizing.flixster.com/J-WXxwHSdv6w7VHxcCrPdebOvUA=/ems.cHJkLWVtcy1hc3NldHMvdHZzZWFzb24vNmE4MmUxOWItNGY5Yy00YzY0LTk5ODktZDY1MGEzZjdmZDFhLmpwZw==" /><meta property="og:locale" content="en_US" /><link rel="canonical" href="https://www.rottentomatoes.com/tv/peacemaker_2022/s02" /><script type="application/ld+json">{"@context":"http://schema.org","@type":"TVSeason","actor":[{"@type":"Person","name":"John Cena","sameAs":"https://www.rottentomatoes.com/celebrity/john_cena","image":"https://resizing.flixster.com/qFr2ZK1qYDkqSmM5eT3nz_n6E_g=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/487578_v9_ba.jpg"},{"@type":"Person","name":"Danielle Brooks","sameAs":"https://www.rottentomatoes.com/celebrity/danielle_brooks","image":"https://resizing.flixster.com/KhnY5vsfjM0vtw0cZL3aNxXbeUE=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/765589_v9_bc.jpg"},{"@type":"Person","name":"Freddie Stroma","sameAs":"https://www.rottentomatoes.com/celebrity/freddie_stroma","image":"https://resizing.flixster.com/Yk2eiDCtamfmNlK-xMa7nmEw_Po=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/GNLZZGG00283ZZD.jpg"},{"@type":"Person","name":"Chukwudi Iwuji","sameAs":"https://www.rottentomatoes.com/celebrity/chukwudi_iwuji","image":"https://resizing.flixster.com/uNAFlG9dNMjJwyMbPDiCsbjkX8I=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/565157_v9_ba.jpg"},{"@type":"Person","name":"Jennifer Holland","sameAs":"https://www.rottentomatoes.com/celebrity/jennifer_holland","image":"https://resizing.flixster.com/-xeYAf0O7fGIQHRx_YkL7vnaMMg=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/331642_v9_bb.jpg"},{"@type":"Person","name":"Steve Agee","sameAs":"https://www.rottentomatoes.com/celebrity/steve_agee","image":"https://resizing.flixster.com/YprPSg0SXNIqq-Wy4UEz4ovBnOw=/100x120/v2/https://resizing.flixster.com/-XZAfHZM39UwaGJIFWKAE8fS0ak=/v3/t/assets/223358_v9_bd.jpg"}],"aggregateRating":{"@type":"AggregateRating","bestRating":"100","description":"The Tomatometer rating โ€“ based on the published opinions of hundreds of film and television critics โ€“ is a trusted measurement of movie and TV programming quality for millions of moviegoers. It represents the percentage of professional critic reviews that are positive for a given film or television show.","name":"Tomatometer","ratingCount":82,"ratingValue":"99","reviewCount":82,"worstRating":"0"},"dateCreated":"2025-08-21","description":"Discover reviews, ratings, and trailers for Peacemaker: Season 2 on Rotten Tomatoes. Stay updated with critic and audience scores today!","episode":[{"@type":"TVEpisode","episodeNumber":"1","url":"https://www.rottentomatoes.com/tv/peacemaker_2022/s02/e01","name":"The Ties That Grind","description":"While Peacemaker attempts to join the Justice Gang, Harcourt struggles to find work, and Economos takes on a challenging new assignment.","dateCreated":"2025-08-21"},{"@type":"TVEpisode","episodeNumber":"2","url":"https://www.rottentomatoes.com/tv/peacemaker_2022/s02/e02","name":"A Man Is Only as Good as His Bird","description":"As Economos clashes with his new handler, Peacemaker must deal with the consequences of his actions in the alternate dimension.","dateCreated":"2025-08-28"},{"@type":"TVEpisode","episodeNumber":"3","url":"https://www.rottentomatoes.com/tv/peacemaker_2022/s02/e03","name":"Another Rick Up My Sleeve","description":"A man fights for peace at any cost, no matter how many people he has to kill to get it.","dateCreated":"2025-09-04"},{"@type":"TVEpisode","episodeNumber":"4","url":"https://www.rottentomatoes.com/tv/peacemaker_2022/s02/e04","name":"Need I Say Door","description":"A man fights for peace at any cost, no matter how many people he has to kill to get it.","dateCreated":"2025-09-11"},{"@type":"TVEpisode","episodeNumber":"5","url":"https://www.rottentomatoes.com/tv/peacemaker_2022/s02/e05","name":"Back to the Suture","description":"A man fights for peace at any cost, no matter how many people he has to kill to get it.","dateCreated":"2025-09-18"},{"@type":"TVEpisode","episodeNumber":"6","url":"https://www.rottentomatoes.com/tv/peacemaker_2022/s02/e06","name":"Ignorance is Chris","description":"A man fights for peace at any cost, no matter how many people he has to kill to get it.","dateCreated":"2025-09-25"},{"@type":"TVEpisode","episodeNumber":"7","url":"https://www.rottentomatoes.com/tv/peacemaker_2022/s02/e07","description":"A man fights for peace at any cost, no matter how many people he has to kill to get it.","dateCreated":"2025-10-02"},{"@type":"TVEpisode","episodeNumber":"8","url":"https://www.rottentomatoes.com/tv/peacemaker_2022/s02/e08","description":"A man fights for peace at any cost, no matter how many people he has to kill to get it.","dateCreated":"2025-10-09"}],"genre":["Comedy","Action"],"image":"https://resizing.flixster.com/J-WXxwHSdv6w7VHxcCrPdebOvUA=/ems.cHJkLWVtcy1hc3NldHMvdHZzZWFzb24vNmE4MmUxOWItNGY5Yy00YzY0LTk5ODktZDY1MGEzZjdmZDFhLmpwZw==","name":"Season 2","partOfSeries":{"@type":"TVSeries","name":"Peacemaker","startDate":"2022-01-13","url":"https://www.rottentomatoes.com/tv/peacemaker_2022"},"producer":[{"@type":"Person","name":"James Gunn","sameAs":"https://www.rottentomatoes.com/celebrity/james_gunn","image":"https://images.fandango.com/cms/assets/b0cefeb0-b6a8-11ed-81d8-51a487a38835--poster-default-thumbnail.jpg"},{"@type":"Person","name":"Peter Safran","sameAs":"https://www.rottentomatoes.com/celebrity/peter_safran","image":"https://images.fandango.com/cms/assets/b0cefeb0-b6a8-11ed-81d8-51a487a38835--poster-default-thumbnail.jpg"}],"url":"https://www.rottentomatoes.com/tv/peacemaker_2022/s02","video":{"@type":"VideoObject","thumbnailUrl":"https://statcdn.fandango.com/MPX/image/NBCU_Fandango/166/403/thumb_47CFBBB8-95A3-4634-8B02-581F2C757D98.jpg","name":"Peacemaker: Season 2 Trailer - Weeks Ahead","duration":"1:39","sourceOrganization":"MPX","uploadDate":"2025-08-28T16:36:57","description":"","contentUrl":"https://www.rottentomatoes.com/tv/peacemaker_2022/s02/videos/nTePljVEct61"}}</script></head><body class="body no-touch js-mptd-layout" data-AdsGlobalSkinTakeoverManager="body" data-SearchResultsNavManager="body"><div id="main" class="container rt-layout__body"><main id="main_container" class="container rt-layout__content"></main><footer class="footer container" data-PagePicturesManager="footer"></footer></div></body></html>
+25 -11
internal/services/test_utilities.go
··· 264 264 265 265 // MockATProtoService is a mock implementation of ATProtoService for testing 266 266 type MockATProtoService struct { 267 - AuthenticateFunc func(ctx context.Context, handle, password string) error 268 - GetSessionFunc func() (*Session, error) 269 - IsAuthenticatedVal bool 270 - RestoreSessionFunc func(session *Session) error 271 - PullDocumentsFunc func(ctx context.Context) ([]DocumentWithMeta, error) 272 - PostDocumentFunc func(ctx context.Context, doc public.Document, isDraft bool) (*DocumentWithMeta, error) 273 - PatchDocumentFunc func(ctx context.Context, rkey string, doc public.Document, isDraft bool) (*DocumentWithMeta, error) 274 - DeleteDocumentFunc func(ctx context.Context, rkey string, isDraft bool) error 275 - UploadBlobFunc func(ctx context.Context, data []byte, mimeType string) (public.Blob, error) 276 - CloseFunc func() error 277 - Session *Session // Exported for test access 267 + AuthenticateFunc func(ctx context.Context, handle, password string) error 268 + GetSessionFunc func() (*Session, error) 269 + IsAuthenticatedVal bool 270 + RestoreSessionFunc func(session *Session) error 271 + PullDocumentsFunc func(ctx context.Context) ([]DocumentWithMeta, error) 272 + PostDocumentFunc func(ctx context.Context, doc public.Document, isDraft bool) (*DocumentWithMeta, error) 273 + PatchDocumentFunc func(ctx context.Context, rkey string, doc public.Document, isDraft bool) (*DocumentWithMeta, error) 274 + DeleteDocumentFunc func(ctx context.Context, rkey string, isDraft bool) error 275 + UploadBlobFunc func(ctx context.Context, data []byte, mimeType string) (public.Blob, error) 276 + GetDefaultPublicationFunc func(ctx context.Context) (string, error) 277 + CloseFunc func() error 278 + Session *Session // Exported for test access 278 279 } 279 280 280 281 // NewMockATProtoService creates a new mock AT Proto service ··· 395 396 MimeType: mimeType, 396 397 Size: len(data), 397 398 }, nil 399 + } 400 + 401 + // GetDefaultPublication mocks getting the default publication 402 + func (m *MockATProtoService) GetDefaultPublication(ctx context.Context) (string, error) { 403 + if m.GetDefaultPublicationFunc != nil { 404 + return m.GetDefaultPublicationFunc(ctx) 405 + } 406 + 407 + // Default returns a mock publication URI 408 + if !m.IsAuthenticatedVal { 409 + return "", errors.New("not authenticated") 410 + } 411 + return "at://did:plc:test123/pub.leaflet.publication/mock_pub_rkey", nil 398 412 } 399 413 400 414 // Close mocks cleanup
+75
internal/shared/shared.go
··· 4 4 import ( 5 5 "errors" 6 6 "fmt" 7 + "io" 8 + "os" 9 + "path/filepath" 10 + "time" 11 + 12 + "github.com/charmbracelet/log" 7 13 ) 8 14 9 15 var ( ··· 17 23 func IsConfigError(err error) bool { 18 24 return errors.Is(err, ErrConfig) 19 25 } 26 + 27 + // CompoundWriter writes every payload to two sinks: 28 + // 1. a primary sink (typically [os.Stdout] or [os.Stderr]) 29 + // 2. a secondary sink (typically [*os.File]) 30 + // 31 + // It satisfies io.Writer. 32 + type CompoundWriter struct { 33 + primary io.Writer 34 + secondary io.Writer 35 + } 36 + 37 + // New creates a new [CompoundWriter] 38 + func New(primary io.Writer, secondary io.Writer) *CompoundWriter { 39 + return &CompoundWriter{ 40 + primary: primary, 41 + secondary: secondary, 42 + } 43 + } 44 + 45 + func LogWithStdErr(w io.WriteCloser) *CompoundWriter { 46 + return New(os.Stderr, w) 47 + } 48 + 49 + func LogWithStdOut(w io.WriteCloser) *CompoundWriter { 50 + return New(os.Stdout, w) 51 + } 52 + 53 + // Write writes p to both instances of [io.Writer] 54 + func (cw *CompoundWriter) Write(p []byte) (int, error) { 55 + var err error 56 + var n1, n2 int 57 + 58 + if n1, err = cw.primary.Write(p); err != nil { 59 + return n1, err 60 + } 61 + if n2, err = cw.secondary.Write(p); err != nil { 62 + return n2, err 63 + } 64 + return len(p), nil 65 + } 66 + 67 + func FallbackLogger() *log.Logger { 68 + return log.NewWithOptions(os.Stderr, log.Options{ 69 + Prefix: "[DEBUG]", 70 + ReportTimestamp: true, 71 + ReportCaller: true, 72 + TimeFormat: time.Kitchen, 73 + Level: log.DebugLevel, 74 + }) 75 + } 76 + 77 + // NewDebugLoggerWithFile creates a new debug logger that writes to both stderr and a log file 78 + func NewDebugLoggerWithFile(configDir string) *log.Logger { 79 + logger := FallbackLogger() 80 + logsDir := filepath.Join(configDir, "logs") 81 + if err := os.MkdirAll(logsDir, 0755); err != nil { 82 + return logger 83 + } 84 + 85 + logFile := filepath.Join(logsDir, fmt.Sprintf("publication_%s.log", time.Now().Format("2006-01-02"))) 86 + file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) 87 + if err != nil { 88 + return logger 89 + } 90 + 91 + w := LogWithStdErr(file) 92 + logger.SetOutput(w) 93 + return logger 94 + }
+260 -39
internal/shared/shared_test.go
··· 1 1 package shared 2 2 3 3 import ( 4 + "bytes" 4 5 "errors" 6 + "io" 5 7 "testing" 6 8 ) 7 9 8 - func TestErrors(t *testing.T) { 9 - t.Run("ConfigError", func(t *testing.T) { 10 - t.Run("creates joined error with message", func(t *testing.T) { 11 - baseErr := errors.New("invalid format") 12 - err := ConfigError("database connection failed", baseErr) 10 + func TestCompoundWriter(t *testing.T) { 11 + t.Run("New", func(t *testing.T) { 12 + t.Run("creates writer with primary and secondary", func(t *testing.T) { 13 + var primary bytes.Buffer 14 + var secondary bytes.Buffer 15 + 16 + cw := New(&primary, &secondary) 13 17 14 - AssertError(t, err, "ConfigError should create an error") 15 - AssertContains(t, err.Error(), "configuration error", "error should contain config error marker") 16 - AssertContains(t, err.Error(), "database connection failed", "error should contain custom message") 17 - AssertContains(t, err.Error(), "invalid format", "error should contain base error") 18 + if cw == nil { 19 + t.Fatal("Expected CompoundWriter to be created") 20 + } 21 + if cw.primary == nil { 22 + t.Error("Expected primary writer to be set") 23 + } 24 + if cw.secondary == nil { 25 + t.Error("Expected secondary writer to be set") 26 + } 18 27 }) 28 + }) 29 + 30 + t.Run("Write", func(t *testing.T) { 31 + t.Run("writes to both primary and secondary", func(t *testing.T) { 32 + var primary bytes.Buffer 33 + var secondary bytes.Buffer 34 + 35 + cw := New(&primary, &secondary) 36 + 37 + testData := []byte("test message") 38 + n, err := cw.Write(testData) 19 39 20 - t.Run("preserves both error chains", func(t *testing.T) { 21 - baseErr := errors.New("connection timeout") 22 - err := ConfigError("failed to connect", baseErr) 40 + if err != nil { 41 + t.Errorf("Expected no error, got %v", err) 42 + } 43 + if n != len(testData) { 44 + t.Errorf("Expected to write %d bytes, got %d", len(testData), n) 45 + } 23 46 24 - AssertTrue(t, errors.Is(err, ErrConfig), "should identify as config error") 25 - AssertTrue(t, errors.Is(err, baseErr), "should preserve original error in chain") 47 + if primary.String() != "test message" { 48 + t.Errorf("Expected primary to contain 'test message', got '%s'", primary.String()) 49 + } 50 + if secondary.String() != "test message" { 51 + t.Errorf("Expected secondary to contain 'test message', got '%s'", secondary.String()) 52 + } 26 53 }) 27 54 28 - t.Run("wraps multiple errors with Join", func(t *testing.T) { 29 - baseErr := errors.New("parse error") 30 - err := ConfigError("invalid config file", baseErr) 55 + t.Run("writes multiple times to both sinks", func(t *testing.T) { 56 + var primary bytes.Buffer 57 + var secondary bytes.Buffer 58 + 59 + cw := New(&primary, &secondary) 31 60 32 - AssertTrue(t, errors.Is(err, ErrConfig), "joined error should contain ErrConfig") 33 - AssertTrue(t, errors.Is(err, baseErr), "joined error should contain base error") 61 + messages := []string{"first", "second", "third"} 62 + for _, msg := range messages { 63 + _, err := cw.Write([]byte(msg)) 64 + if err != nil { 65 + t.Errorf("Expected no error writing '%s', got %v", msg, err) 66 + } 67 + } 68 + 69 + expected := "firstsecondthird" 70 + if primary.String() != expected { 71 + t.Errorf("Expected primary to contain '%s', got '%s'", expected, primary.String()) 72 + } 73 + if secondary.String() != expected { 74 + t.Errorf("Expected secondary to contain '%s', got '%s'", expected, secondary.String()) 75 + } 34 76 }) 35 - }) 77 + 78 + t.Run("returns error from primary writer", func(t *testing.T) { 79 + var secondary bytes.Buffer 80 + expectedErr := errors.New("primary write failed") 81 + primary := &errorWriter{err: expectedErr} 82 + 83 + cw := New(primary, &secondary) 84 + 85 + _, err := cw.Write([]byte("test")) 86 + 87 + if err == nil { 88 + t.Error("Expected error from primary writer") 89 + } 90 + if !errors.Is(err, expectedErr) { 91 + t.Errorf("Expected error '%v', got '%v'", expectedErr, err) 92 + } 93 + }) 94 + 95 + t.Run("returns error from secondary writer", func(t *testing.T) { 96 + var primary bytes.Buffer 97 + expectedErr := errors.New("secondary write failed") 98 + secondary := &errorWriter{err: expectedErr} 99 + 100 + cw := New(&primary, secondary) 101 + 102 + _, err := cw.Write([]byte("test")) 103 + 104 + if err == nil { 105 + t.Error("Expected error from secondary writer") 106 + } 107 + if !errors.Is(err, expectedErr) { 108 + t.Errorf("Expected error '%v', got '%v'", expectedErr, err) 109 + } 110 + }) 36 111 37 - t.Run("IsConfigError", func(t *testing.T) { 38 - t.Run("identifies config errors", func(t *testing.T) { 39 - baseErr := errors.New("test error") 40 - err := ConfigError("test message", baseErr) 112 + t.Run("writes to primary even if secondary fails", func(t *testing.T) { 113 + var primary bytes.Buffer 114 + expectedErr := errors.New("secondary write failed") 115 + secondary := &errorWriter{err: expectedErr} 116 + 117 + cw := New(&primary, secondary) 118 + 119 + testData := []byte("test message") 120 + _, _ = cw.Write(testData) 41 121 42 - AssertTrue(t, IsConfigError(err), "should identify config error") 122 + if primary.String() != "test message" { 123 + t.Errorf("Expected primary to contain 'test message' even with secondary error, got '%s'", primary.String()) 124 + } 43 125 }) 44 126 45 - t.Run("returns false for regular errors", func(t *testing.T) { 46 - err := errors.New("regular error") 127 + t.Run("handles empty write", func(t *testing.T) { 128 + var primary bytes.Buffer 129 + var secondary bytes.Buffer 130 + 131 + cw := New(&primary, &secondary) 47 132 48 - AssertFalse(t, IsConfigError(err), "should not identify regular error as config error") 133 + n, err := cw.Write([]byte{}) 134 + 135 + if err != nil { 136 + t.Errorf("Expected no error on empty write, got %v", err) 137 + } 138 + if n != 0 { 139 + t.Errorf("Expected to write 0 bytes, got %d", n) 140 + } 49 141 }) 50 142 51 - t.Run("returns false for nil error", func(t *testing.T) { 52 - AssertFalse(t, IsConfigError(nil), "should return false for nil error") 143 + t.Run("handles large write", func(t *testing.T) { 144 + var primary bytes.Buffer 145 + var secondary bytes.Buffer 146 + 147 + cw := New(&primary, &secondary) 148 + 149 + largeData := make([]byte, 1024*1024) // 1MB 150 + for i := range largeData { 151 + largeData[i] = byte(i % 256) 152 + } 153 + 154 + n, err := cw.Write(largeData) 155 + 156 + if err != nil { 157 + t.Errorf("Expected no error on large write, got %v", err) 158 + } 159 + if n != len(largeData) { 160 + t.Errorf("Expected to write %d bytes, got %d", len(largeData), n) 161 + } 162 + 163 + if !bytes.Equal(primary.Bytes(), largeData) { 164 + t.Error("Primary writer didn't receive correct data") 165 + } 166 + if !bytes.Equal(secondary.Bytes(), largeData) { 167 + t.Error("Secondary writer didn't receive correct data") 168 + } 53 169 }) 170 + }) 171 + 172 + t.Run("WithStdErr", func(t *testing.T) { 173 + t.Run("creates writer with stderr as primary", func(t *testing.T) { 174 + var buf bytes.Buffer 175 + closer := &nopCloser{Writer: &buf} 176 + 177 + cw := LogWithStdErr(closer) 178 + 179 + if cw == nil { 180 + t.Fatal("Expected CompoundWriter to be created") 181 + } 54 182 55 - t.Run("returns false for wrapped non-config errors", func(t *testing.T) { 56 - baseErr := errors.New("base error") 57 - wrappedErr := errors.New("wrapped: " + baseErr.Error()) 183 + testData := []byte("test") 184 + _, err := cw.Write(testData) 185 + if err != nil { 186 + t.Errorf("Expected no error, got %v", err) 187 + } 58 188 59 - AssertFalse(t, IsConfigError(wrappedErr), "should not identify wrapped non-config error") 189 + if buf.String() != "test" { 190 + t.Errorf("Expected secondary to contain 'test', got '%s'", buf.String()) 191 + } 60 192 }) 193 + }) 61 194 62 - t.Run("identifies wrapped config errors", func(t *testing.T) { 63 - baseErr := errors.New("original error") 64 - configErr := ConfigError("config issue", baseErr) 65 - wrappedAgain := errors.Join(errors.New("outer error"), configErr) 195 + t.Run("WithStdOut", func(t *testing.T) { 196 + t.Run("creates writer with stdout as primary", func(t *testing.T) { 197 + var buf bytes.Buffer 198 + closer := &nopCloser{Writer: &buf} 199 + 200 + cw := LogWithStdOut(closer) 201 + 202 + if cw == nil { 203 + t.Fatal("Expected CompoundWriter to be created") 204 + } 66 205 67 - AssertTrue(t, IsConfigError(wrappedAgain), "should identify config error in join chain") 206 + testData := []byte("test") 207 + _, err := cw.Write(testData) 208 + if err != nil { 209 + t.Errorf("Expected no error, got %v", err) 210 + } 211 + 212 + if buf.String() != "test" { 213 + t.Errorf("Expected secondary to contain 'test', got '%s'", buf.String()) 214 + } 68 215 }) 69 216 }) 70 217 } 218 + 219 + func TestConfigError(t *testing.T) { 220 + t.Run("wraps error with message", func(t *testing.T) { 221 + originalErr := errors.New("original error") 222 + configErr := ConfigError("test message", originalErr) 223 + 224 + if configErr == nil { 225 + t.Fatal("Expected error to be returned") 226 + } 227 + 228 + errMsg := configErr.Error() 229 + if errMsg != "configuration error\ntest message: original error" { 230 + t.Errorf("Expected specific error format, got '%s'", errMsg) 231 + } 232 + }) 233 + 234 + t.Run("is detectable with IsConfigError", func(t *testing.T) { 235 + originalErr := errors.New("original error") 236 + configErr := ConfigError("test message", originalErr) 237 + 238 + if !IsConfigError(configErr) { 239 + t.Error("Expected IsConfigError to return true") 240 + } 241 + }) 242 + 243 + t.Run("wraps original error", func(t *testing.T) { 244 + originalErr := errors.New("original error") 245 + configErr := ConfigError("test message", originalErr) 246 + 247 + if !errors.Is(configErr, originalErr) { 248 + t.Error("Expected config error to wrap original error") 249 + } 250 + }) 251 + } 252 + 253 + func TestIsConfigError(t *testing.T) { 254 + t.Run("returns true for config error", func(t *testing.T) { 255 + configErr := ConfigError("test", errors.New("inner")) 256 + 257 + if !IsConfigError(configErr) { 258 + t.Error("Expected IsConfigError to return true for config error") 259 + } 260 + }) 261 + 262 + t.Run("returns false for non-config error", func(t *testing.T) { 263 + regularErr := errors.New("regular error") 264 + 265 + if IsConfigError(regularErr) { 266 + t.Error("Expected IsConfigError to return false for regular error") 267 + } 268 + }) 269 + 270 + t.Run("returns false for nil error", func(t *testing.T) { 271 + if IsConfigError(nil) { 272 + t.Error("Expected IsConfigError to return false for nil error") 273 + } 274 + }) 275 + } 276 + 277 + type errorWriter struct { 278 + err error 279 + } 280 + 281 + func (w *errorWriter) Write(p []byte) (int, error) { 282 + return 0, w.err 283 + } 284 + 285 + type nopCloser struct { 286 + io.Writer 287 + } 288 + 289 + func (nc *nopCloser) Close() error { 290 + return nil 291 + }
+2
internal/store/sql/migrations/0009_add_wait_scheduled_to_tasks_down.sql
··· 1 + ALTER TABLE tasks DROP COLUMN wait; 2 + ALTER TABLE tasks DROP COLUMN scheduled;
+2
internal/store/sql/migrations/0009_add_wait_scheduled_to_tasks_up.sql
··· 1 + ALTER TABLE tasks ADD COLUMN wait DATETIME; 2 + ALTER TABLE tasks ADD COLUMN scheduled DATETIME;
+3
internal/store/sql/migrations/0010_create_documents_table_down.sql
··· 1 + DROP INDEX IF EXISTS idx_documents_created_at; 2 + DROP INDEX IF EXISTS idx_documents_doc_kind; 3 + DROP TABLE IF EXISTS documents;
+10
internal/store/sql/migrations/0010_create_documents_table_up.sql
··· 1 + CREATE TABLE IF NOT EXISTS documents ( 2 + id INTEGER PRIMARY KEY AUTOINCREMENT, 3 + title TEXT NOT NULL, 4 + body TEXT NOT NULL, 5 + created_at DATETIME NOT NULL, 6 + doc_kind INTEGER NOT NULL 7 + ); 8 + 9 + CREATE INDEX IF NOT EXISTS idx_documents_doc_kind ON documents(doc_kind); 10 + CREATE INDEX IF NOT EXISTS idx_documents_created_at ON documents(created_at);
+1
internal/store/sql/migrations/0011_create_task_history_table_down.sql
··· 1 + DROP TABLE IF EXISTS task_history;
+11
internal/store/sql/migrations/0011_create_task_history_table_up.sql
··· 1 + -- Task history table for undo functionality 2 + CREATE TABLE IF NOT EXISTS task_history ( 3 + id INTEGER PRIMARY KEY AUTOINCREMENT, 4 + task_id INTEGER NOT NULL, 5 + operation TEXT NOT NULL, -- 'update', 'delete' 6 + snapshot TEXT NOT NULL, -- JSON snapshot of task before operation 7 + created_at DATETIME DEFAULT CURRENT_TIMESTAMP 8 + ); 9 + 10 + CREATE INDEX IF NOT EXISTS idx_task_history_task_id ON task_history(task_id); 11 + CREATE INDEX IF NOT EXISTS idx_task_history_created_at ON task_history(created_at);
+8 -26
internal/ui/auth_form.go
··· 9 9 "github.com/charmbracelet/bubbles/key" 10 10 "github.com/charmbracelet/bubbles/textinput" 11 11 tea "github.com/charmbracelet/bubbletea" 12 - "github.com/charmbracelet/lipgloss" 13 12 ) 14 13 15 14 // AuthFormOptions configures the auth form display ··· 171 170 func (m authFormModel) View() string { 172 171 var b strings.Builder 173 172 174 - titleStyle := lipgloss.NewStyle(). 175 - Bold(true). 176 - Foreground(lipgloss.Color("6")). 177 - MarginBottom(1) 178 - 179 - labelStyle := lipgloss.NewStyle(). 180 - Foreground(lipgloss.Color("7")) 181 - 182 - helpStyle := lipgloss.NewStyle(). 183 - Foreground(lipgloss.Color("8")). 184 - MarginTop(1) 185 - 186 - errorStyle := lipgloss.NewStyle(). 187 - Foreground(lipgloss.Color("9")) 188 - 189 - b.WriteString(titleStyle.Render("AT Protocol Authentication")) 173 + b.WriteString(TitleStyle.Render("AT Protocol Authentication")) 190 174 b.WriteString("\n\n") 191 175 192 - b.WriteString(labelStyle.Render("BlueSky Handle:")) 176 + b.WriteString(TextStyle.Render("BlueSky Handle:")) 193 177 b.WriteString("\n") 194 178 if m.handleLocked { 195 - lockedStyle := lipgloss.NewStyle(). 196 - Foreground(lipgloss.Color("8")) 197 - b.WriteString(lockedStyle.Render(m.handleInput.Value())) 198 - b.WriteString(lockedStyle.Render(" (locked)")) 179 + b.WriteString(MutedStyle.Render(m.handleInput.Value())) 180 + b.WriteString(MutedStyle.Render(" (locked)")) 199 181 } else { 200 182 b.WriteString(m.handleInput.View()) 201 183 } 202 184 b.WriteString("\n\n") 203 185 204 - b.WriteString(labelStyle.Render("App Password:")) 186 + b.WriteString(TextStyle.Render("App Password:")) 205 187 b.WriteString("\n") 206 188 b.WriteString(m.passwordInput.View()) 207 189 b.WriteString("\n\n") 208 190 209 191 if m.handleInput.Value() == "" { 210 - b.WriteString(errorStyle.Render("Handle is required")) 192 + b.WriteString(ErrorStyle.Render("Handle is required")) 211 193 b.WriteString("\n") 212 194 } 213 195 if m.passwordInput.Value() == "" { 214 - b.WriteString(errorStyle.Render("Password is required")) 196 + b.WriteString(ErrorStyle.Render("Password is required")) 215 197 b.WriteString("\n") 216 198 } 217 199 218 200 b.WriteString("\n") 219 201 helpText := "tab/shift+tab: navigate โ€ข enter/ctrl+s: submit โ€ข esc/ctrl+c: cancel" 220 - b.WriteString(helpStyle.Render(helpText)) 202 + b.WriteString(MutedStyle.MarginTop(1).Render(helpText)) 221 203 222 204 return b.String() 223 205 }
+36 -4
internal/ui/common.go
··· 1 - // TODO: create variants of colored output without icons 2 - // TODO: refactor existing (relevant) calls to old styles 3 - // TODO: k v wrappers 4 1 package ui 5 2 6 3 import ( ··· 9 6 "github.com/charmbracelet/lipgloss" 10 7 ) 11 8 9 + // Style constructor helpers 12 10 func newStyle() lipgloss.Style { return lipgloss.NewStyle() } 13 11 func newPStyle(v, h int) lipgloss.Style { return lipgloss.NewStyle().Padding(v, h) } 14 12 func newBoldStyle() lipgloss.Style { return newStyle().Bold(true) } 15 13 func newPBoldStyle(v, h int) lipgloss.Style { return newPStyle(v, h).Bold(true) } 16 14 func newEmStyle() lipgloss.Style { return newStyle().Italic(true) } 17 15 16 + // Rendering helpers (private, used by public API) 18 17 func success(msg string) string { return SuccessStyle.Render("โœ“ " + msg) } 19 18 func errorMsg(msg string) string { return ErrorStyle.Render("โœ— " + msg) } 20 19 func warning(msg string) string { return WarningStyle.Render("โš  " + msg) } ··· 25 24 func box(content string) string { return BoxStyle.Render(content) } 26 25 func errorBox(content string) string { return ErrorBoxStyle.Render(content) } 27 26 func text(content string) string { return TextStyle.Render(content) } 27 + func muted(content string) string { return MutedStyle.Render(content) } 28 + func accent(content string) string { return AccentStyle.Render(content) } 28 29 func header(content string) string { return HeaderStyle.Render(content) } 30 + func primary(content string) string { return PrimaryStyle.Render(content) } 29 31 30 32 // Success prints a formatted success message 31 33 func Success(format string, a ...any) { ··· 121 123 func Plain(format string, a ...any) { fmt.Print(text(fmt.Sprintf(format, a...))) } 122 124 func Plainln(format string, a ...any) { fmt.Println(text(fmt.Sprintf(format, a...))) } 123 125 func Header(format string, a ...any) { fmt.Print(header(fmt.Sprintf(format, a...))) } 124 - func Headerln(format string, a ...any) { fmt.Print(header(fmt.Sprintf(format, a...))) } 126 + func Headerln(format string, a ...any) { fmt.Println(header(fmt.Sprintf(format, a...))) } 127 + 128 + // Muted prints muted/secondary text 129 + func Muted(format string, a ...any) { 130 + fmt.Print(muted(fmt.Sprintf(format, a...))) 131 + } 132 + 133 + // Mutedln prints muted/secondary text with a newline 134 + func Mutedln(format string, a ...any) { 135 + fmt.Println(muted(fmt.Sprintf(format, a...))) 136 + } 137 + 138 + // Accent prints accent-colored text 139 + func Accent(format string, a ...any) { 140 + fmt.Print(accent(fmt.Sprintf(format, a...))) 141 + } 142 + 143 + // Accentln prints accent-colored text with a newline 144 + func Accentln(format string, a ...any) { 145 + fmt.Println(accent(fmt.Sprintf(format, a...))) 146 + } 147 + 148 + // Primary prints primary-colored text 149 + func Primary(format string, a ...any) { 150 + fmt.Print(primary(fmt.Sprintf(format, a...))) 151 + } 152 + 153 + // Primaryln prints primary-colored text with a newline 154 + func Primaryln(format string, a ...any) { 155 + fmt.Println(primary(fmt.Sprintf(format, a...))) 156 + }
+3 -4
internal/ui/data_list.go
··· 11 11 "github.com/charmbracelet/bubbles/key" 12 12 "github.com/charmbracelet/bubbles/viewport" 13 13 tea "github.com/charmbracelet/bubbletea" 14 - "github.com/charmbracelet/lipgloss" 15 14 "github.com/stormlightlabs/noteleaf/internal/models" 16 15 ) 17 16 ··· 315 314 func (m dataListModel) View() string { 316 315 var s strings.Builder 317 316 318 - style := lipgloss.NewStyle().Foreground(lipgloss.Color(Squid.Hex())) 317 + style := MutedStyle 319 318 320 319 if m.showingHelp { 321 320 return m.help.View(m.keys) ··· 328 327 return s.String() 329 328 } 330 329 331 - s.WriteString(TitleColorStyle.Render(m.opts.Title)) 330 + s.WriteString(TableTitleStyle.Render(m.opts.Title)) 332 331 if m.totalCount > 0 { 333 332 s.WriteString(fmt.Sprintf(" (%d total)", m.totalCount)) 334 333 } ··· 434 433 } 435 434 436 435 if selected { 437 - return SelectedColorStyle.Render(line) 436 + return TableSelectedStyle.Render(line) 438 437 } 439 438 return line 440 439 }
+5 -6
internal/ui/data_table.go
··· 10 10 "github.com/charmbracelet/bubbles/help" 11 11 "github.com/charmbracelet/bubbles/key" 12 12 tea "github.com/charmbracelet/bubbletea" 13 - "github.com/charmbracelet/lipgloss" 14 13 "github.com/stormlightlabs/noteleaf/internal/models" 15 14 ) 16 15 ··· 258 257 func (m dataTableModel) View() string { 259 258 var s strings.Builder 260 259 261 - style := lipgloss.NewStyle().Foreground(lipgloss.Color(Squid.Hex())) 260 + style := MutedStyle 262 261 263 262 if m.showingHelp { 264 263 return m.help.View(m.keys) ··· 271 270 return s.String() 272 271 } 273 272 274 - s.WriteString(TitleColorStyle.Render(m.opts.Title)) 273 + s.WriteString(TableTitleStyle.Render(m.opts.Title)) 275 274 if m.totalCount > 0 { 276 275 s.WriteString(fmt.Sprintf(" (%d total)", m.totalCount)) 277 276 } ··· 300 299 headerParts[i] = fmt.Sprintf(format, field.Title) 301 300 } 302 301 headerLine := fmt.Sprintf(" %s", strings.Join(headerParts, " ")) 303 - s.WriteString(HeaderColorStyle.Render(headerLine)) 302 + s.WriteString(TableHeaderStyle.Render(headerLine)) 304 303 s.WriteString("\n") 305 304 306 305 totalWidth := 3 + len(strings.Join(headerParts, " ")) 307 - s.WriteString(HeaderColorStyle.Render(strings.Repeat("โ”€", totalWidth))) 306 + s.WriteString(TableHeaderStyle.Render(strings.Repeat("โ”€", totalWidth))) 308 307 s.WriteString("\n") 309 308 310 309 for i, record := range m.records { ··· 335 334 line := fmt.Sprintf("%s%s", prefix, strings.Join(rowParts, " ")) 336 335 337 336 if i == m.selected { 338 - s.WriteString(SelectedColorStyle.Render(line)) 337 + s.WriteString(TableSelectedStyle.Render(line)) 339 338 } else { 340 339 s.WriteString(style.Render(line)) 341 340 }
+80 -52
internal/ui/palette.go
··· 19 19 20 20 var NoteleafColorScheme fang.ColorSchemeFunc = noteleafColorScheme 21 21 22 + // noteleafColorScheme provides Iceberg-inspired colors for CLI help/documentation 23 + // 24 + // Philosophy: Cool blues as primary, warm accents for emphasis, hierarchical text colors 25 + // See: https://github.com/cocopon/iceberg.vim for more information 22 26 func noteleafColorScheme(c lipglossv2.LightDarkFunc) fang.ColorScheme { 23 27 return fang.ColorScheme{ 24 - Base: c(Salt, Pepper), // Light/Dark base text 25 - Title: c(Guac, Julep), // Green primary for titles 26 - Description: c(Squid, Smoke), // Muted gray for descriptions 27 - Codeblock: c(Butter, BBQ), // Light/Dark background for code 28 - Program: c(Malibu, Sardine), // Blue for program names 29 - DimmedArgument: c(Oyster, Ash), // Subtle gray for dimmed text 30 - Comment: c(Pickle, NeueGuac), // Green for comments 31 - Flag: c(Violet, Mauve), // Purple for flags 32 - FlagDefault: c(Lichen, Turtle), // Teal for flag defaults 33 - Command: c(Julep, Guac), // Bright green for commands 34 - QuotedString: c(Citron, Mustard), // Yellow for quoted strings 35 - Argument: c(Sapphire, Guppy), // Blue for arguments 36 - Help: c(Smoke, Iron), // Gray for help text 37 - Dash: c(Iron, Oyster), // Medium gray for dashes 38 - ErrorHeader: [2]color.Color{Cherry, Sriracha}, // Red for error headers (fg, bg) 39 - ErrorDetails: c(Coral, Salmon), // Red/pink for error details 28 + Base: c(Salt, Pepper), // Primary text on dark background 29 + Title: c(Malibu, Malibu), // Blue primary for titles (Iceberg primary) 30 + Description: c(Smoke, Smoke), // Secondary text for descriptions 31 + Codeblock: c(Butter, BBQ), // Light/Dark background for code blocks 32 + Program: c(Malibu, Sardine), // Blue for program names (primary accent) 33 + DimmedArgument: c(Oyster, Ash), // Dimmed text for optional arguments 34 + Comment: c(Squid, Squid), // Muted gray for comments (Iceberg comment) 35 + Flag: c(Hazy, Jelly), // Purple for flags (Iceberg special) 36 + FlagDefault: c(Lichen, Turtle), // Teal for flag defaults (secondary accent) 37 + Command: c(Julep, Julep), // Green for commands (success/positive) 38 + QuotedString: c(Tang, Tang), // Orange for quoted strings (warning/warm) 39 + Argument: c(Lichen, Lichen), // Teal for arguments (secondary accent) 40 + Help: c(Squid, Squid), // Muted gray for help text 41 + Dash: c(Oyster, Oyster), // Dimmed gray for dashes/separators 42 + ErrorHeader: [2]color.Color{Sriracha, Sriracha}, // Red for error headers (Iceberg error) 43 + ErrorDetails: c(Coral, Salmon), // Pink/coral for error details 40 44 } 41 45 } 42 46 ··· 53 57 } 54 58 55 59 var ( 56 - ColorPrimary = Thunder.Hex() // Blue 57 - ColorAccent = Cumin.Hex() // Yellow/Gold 58 - ColorError = Paprika.Hex() // Red/Pink 59 - ColorText = Salt.Hex() // Light text 60 - ColorBG = Pepper.Hex() // Dark background 60 + ColorBGBase = Pepper.Hex() // #201F26 - Darkest base 61 + ColorBGSecondary = BBQ.Hex() // #2d2c35 - Secondary background 62 + ColorBGTertiary = Charcoal.Hex() // #3A3943 - Tertiary/elevated 63 + ColorBGInput = Iron.Hex() // #4D4C57 - Input fields/focus 61 64 62 - PrimaryStyle = newStyle().Foreground(lipgloss.Color(ColorPrimary)) 63 - AccentStyle = newStyle().Foreground(lipgloss.Color(ColorAccent)) 64 - ErrorStyle = newStyle().Foreground(lipgloss.Color(ColorError)) 65 - TextStyle = newStyle().Foreground(lipgloss.Color(ColorText)) 66 - TitleStyle = newPBoldStyle(0, 1).Foreground(lipgloss.Color(ColorAccent)) 67 - SubtitleStyle = newEmStyle().Foreground(lipgloss.Color(ColorPrimary)) 68 - SuccessStyle = newBoldStyle().Foreground(lipgloss.Color(ColorPrimary)) 69 - WarningStyle = newBoldStyle().Foreground(lipgloss.Color(ColorAccent)) 70 - InfoStyle = newStyle().Foreground(lipgloss.Color(ColorText)) 71 - BoxStyle = newPStyle(1, 2).Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color(ColorPrimary)) 72 - ErrorBoxStyle = newPStyle(1, 2).Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color(ColorError)) 73 - ListItemStyle = newStyle().Foreground(lipgloss.Color(ColorText)).PaddingLeft(2) 74 - SelectedItemStyle = newBoldStyle().Foreground(lipgloss.Color(ColorAccent)).PaddingLeft(2) 75 - HeaderStyle = newPBoldStyle(0, 1).Foreground(lipgloss.Color(ColorPrimary)) 76 - CellStyle = newPStyle(0, 1).Foreground(lipgloss.Color(ColorText)) 65 + ColorTextPrimary = Salt.Hex() // #F1EFEF - Primary text (brightest) 66 + ColorTextSecondary = Smoke.Hex() // #BFBCC8 - Secondary text 67 + ColorTextMuted = Squid.Hex() // #858392 - Muted/comments 68 + ColorTextDimmed = Oyster.Hex() // #605F6B - Dimmed text 77 69 78 - TaskTitleStyle = newBoldStyle().Foreground(lipgloss.Color(Salt.Hex())) 79 - TaskIDStyle = newStyle().Foreground(lipgloss.Color(Squid.Hex())).Width(8) 70 + ColorPrimary = Malibu.Hex() // #00A4FF - Blue (primary accent) 71 + ColorSuccess = Julep.Hex() // #00FFB2 - Green (success/positive) 72 + ColorError = Sriracha.Hex() // #EB4268 - Red (errors) 73 + ColorWarning = Tang.Hex() // #FF985A - Orange (warnings) 74 + ColorInfo = Violet.Hex() // #C259FF - Purple (info) 75 + ColorAccent = Lichen.Hex() // #5CDFEA - Teal (secondary accent) 80 76 81 - StatusPending = newStyle().Foreground(lipgloss.Color(Citron.Hex())) 82 - StatusCompleted = newStyle().Foreground(lipgloss.Color(Julep.Hex())) 77 + PrimaryStyle = newStyle().Foreground(lipgloss.Color(ColorPrimary)) 78 + SuccessStyle = newBoldStyle().Foreground(lipgloss.Color(ColorSuccess)) 79 + ErrorStyle = newBoldStyle().Foreground(lipgloss.Color(ColorError)) 80 + WarningStyle = newBoldStyle().Foreground(lipgloss.Color(ColorWarning)) 81 + InfoStyle = newStyle().Foreground(lipgloss.Color(ColorTextSecondary)) 82 + AccentStyle = newStyle().Foreground(lipgloss.Color(ColorAccent)) 83 + TextStyle = newStyle().Foreground(lipgloss.Color(ColorTextPrimary)) 84 + MutedStyle = newStyle().Foreground(lipgloss.Color(ColorTextMuted)) 85 + TitleStyle = newPBoldStyle(0, 1).Foreground(lipgloss.Color(ColorPrimary)) 86 + SubtitleStyle = newEmStyle().Foreground(lipgloss.Color(ColorAccent)) 83 87 84 - PriorityHigh = newBoldStyle().Foreground(lipgloss.Color(Cherry.Hex())) 85 - PriorityMedium = newStyle().Foreground(lipgloss.Color(Citron.Hex())) 86 - PriorityLow = newStyle().Foreground(lipgloss.Color(Squid.Hex())) 88 + BoxStyle = newPStyle(1, 2).Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color(ColorPrimary)) 89 + ErrorBoxStyle = newPStyle(1, 2).Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color(ColorError)) 90 + HeaderStyle = newPBoldStyle(0, 1).Foreground(lipgloss.Color(ColorPrimary)) 91 + CellStyle = newPStyle(0, 1).Foreground(lipgloss.Color(ColorTextPrimary)) 87 92 88 - MovieStyle = newBoldStyle().Foreground(lipgloss.Color(Coral.Hex())) 89 - TVStyle = newBoldStyle().Foreground(lipgloss.Color(Violet.Hex())) 90 - BookStyle = newBoldStyle().Foreground(lipgloss.Color(Guac.Hex())) 91 - MusicStyle = newBoldStyle().Foreground(lipgloss.Color(Lichen.Hex())) 93 + ListItemStyle = newStyle().Foreground(lipgloss.Color(ColorTextPrimary)).PaddingLeft(2) 94 + SelectedItemStyle = newBoldStyle().Foreground(lipgloss.Color(ColorPrimary)).PaddingLeft(2) 92 95 93 - TableStyle = newStyle().BorderStyle(lipgloss.NormalBorder()).BorderForeground(lipgloss.Color(Smoke.Hex())) 94 - SelectedStyle = newBoldStyle().Foreground(lipgloss.Color(Salt.Hex())).Background(lipgloss.Color(Squid.Hex())) 95 - TitleColorStyle = newBoldStyle().Foreground(lipgloss.Color("212")) 96 - SelectedColorStyle = newBoldStyle().Foreground(lipgloss.Color("0")).Background(lipgloss.Color("212")) 97 - HeaderColorStyle = newBoldStyle().Foreground(lipgloss.Color("240")) 96 + TableStyle = newStyle().BorderStyle(lipgloss.NormalBorder()).BorderForeground(lipgloss.Color(ColorTextMuted)) 97 + TableHeaderStyle = newBoldStyle().Foreground(lipgloss.Color(ColorAccent)) 98 + TableTitleStyle = newBoldStyle().Foreground(lipgloss.Color(ColorPrimary)) 99 + TableSelectedStyle = newBoldStyle().Foreground(lipgloss.Color(ColorTextPrimary)).Background(lipgloss.Color(ColorBGInput)) 100 + 101 + TaskTitleStyle = newBoldStyle().Foreground(lipgloss.Color(ColorTextPrimary)) 102 + TaskIDStyle = newStyle().Foreground(lipgloss.Color(ColorTextMuted)).Width(8) 103 + 104 + StatusTodo = newStyle().Foreground(lipgloss.Color(ColorTextMuted)) // Gray (muted) 105 + StatusInProgress = newStyle().Foreground(lipgloss.Color(ColorPrimary)) // Blue (active) 106 + StatusBlocked = newStyle().Foreground(lipgloss.Color(ColorError)) // Red (blocked) 107 + StatusDone = newStyle().Foreground(lipgloss.Color(ColorSuccess)) // Green (success) 108 + StatusPending = newStyle().Foreground(lipgloss.Color(ColorWarning)) // Orange (pending) 109 + StatusCompleted = newStyle().Foreground(lipgloss.Color(ColorSuccess)) // Green (completed) 110 + StatusAbandoned = newStyle().Foreground(lipgloss.Color(ColorTextDimmed)) // Dimmed gray (abandoned) 111 + StatusDeleted = newStyle().Foreground(lipgloss.Color(Pom.Hex())) // Dark red (deleted) 112 + 113 + PriorityHigh = newBoldStyle().Foreground(lipgloss.Color(Pom.Hex())) // #FF388B - Bright red 114 + PriorityMedium = newStyle().Foreground(lipgloss.Color(Tang.Hex())) // #FF985A - Orange 115 + PriorityLow = newStyle().Foreground(lipgloss.Color(ColorAccent)) // #5CDFEA - Teal (low) 116 + PriorityNone = newStyle().Foreground(lipgloss.Color(ColorTextMuted)) // #858392 - Gray (no priority) 117 + PriorityLegacy = newStyle().Foreground(lipgloss.Color(Urchin.Hex())) // #C337E0 - Magenta (legacy) 118 + 119 + MovieStyle = newBoldStyle().Foreground(lipgloss.Color(Coral.Hex())) // #FF577D - Pink/coral 120 + TVStyle = newBoldStyle().Foreground(lipgloss.Color(Violet.Hex())) // #C259FF - Purple 121 + BookStyle = newBoldStyle().Foreground(lipgloss.Color(Guac.Hex())) // #12C78F - Green 122 + MusicStyle = newBoldStyle().Foreground(lipgloss.Color(Lichen.Hex())) // #5CDFEA - Teal 123 + 124 + AdditionStyle = newStyle().Foreground(lipgloss.Color(Pickle.Hex())) // #00A475 - Green 125 + DeletionStyle = newStyle().Foreground(lipgloss.Color(Pom.Hex())) // #AB2454 - Red 98 126 )
+18 -11
internal/ui/publication_list_adapter.go
··· 170 170 return NewPublicationDataList(repo, opts, filter) 171 171 } 172 172 173 - // formatPublicationForView formats a publication for display with glamour 174 - func formatPublicationForView(note *models.Note) string { 173 + // buildPublicationMarkdown builds markdown content for a publication without rendering 174 + func buildPublicationMarkdown(note *models.Note) string { 175 175 var content strings.Builder 176 176 177 177 content.WriteString("# " + note.Title + "\n\n") ··· 180 180 if note.IsDraft { 181 181 status = "draft" 182 182 } 183 - content.WriteString("**Status:** " + status + "\n") 183 + content.WriteString("- **Status:** " + status + "\n") 184 184 185 185 if note.PublishedAt != nil { 186 - content.WriteString("**Published:** " + note.PublishedAt.Format("2006-01-02 15:04") + "\n") 186 + content.WriteString("- **Published:** " + note.PublishedAt.Format("2006-01-02 15:04") + "\n") 187 187 } 188 188 189 - content.WriteString("**Modified:** " + note.Modified.Format("2006-01-02 15:04") + "\n") 189 + content.WriteString("- **Modified:** " + note.Modified.Format("2006-01-02 15:04") + "\n") 190 190 191 191 if note.LeafletRKey != nil { 192 - content.WriteString("**RKey:** `" + *note.LeafletRKey + "`\n") 192 + content.WriteString("- **RKey:** `" + ObfuscateMiddle(*note.LeafletRKey, 3, 3) + "`\n") 193 193 } 194 194 195 195 if note.LeafletCID != nil { 196 - content.WriteString("**CID:** `" + *note.LeafletCID + "`\n") 196 + content.WriteString("- **CID:** `" + ObfuscateMiddle(*note.LeafletCID, 3, 3) + "`\n") 197 197 } 198 198 199 199 content.WriteString("\n---\n\n") ··· 208 208 } 209 209 } 210 210 211 + return content.String() 212 + } 213 + 214 + // formatPublicationForView formats a publication for display with glamour 215 + func formatPublicationForView(note *models.Note) string { 216 + markdown := buildPublicationMarkdown(note) 217 + 211 218 renderer, err := glamour.NewTermRenderer( 212 - glamour.WithAutoStyle(), 219 + glamour.WithStandardStyle("tokyo-night"), 213 220 glamour.WithWordWrap(80), 214 221 ) 215 222 if err != nil { 216 - return content.String() 223 + return markdown 217 224 } 218 225 219 - rendered, err := renderer.Render(content.String()) 226 + rendered, err := renderer.Render(markdown) 220 227 if err != nil { 221 - return content.String() 228 + return markdown 222 229 } 223 230 224 231 return rendered
+18 -24
internal/ui/publication_list_adapter_test.go
··· 493 493 } 494 494 }) 495 495 496 - t.Run("formatPublicationForView", func(t *testing.T) { 496 + t.Run("buildPublicationMarkdown", func(t *testing.T) { 497 497 t.Run("formats published note with all metadata", func(t *testing.T) { 498 498 rkey := "test-rkey" 499 499 cid := "test-cid" ··· 510 510 LeafletCID: &cid, 511 511 } 512 512 513 - result := formatPublicationForView(note) 513 + result := buildPublicationMarkdown(note) 514 514 515 515 if !strings.Contains(result, "Test Article") { 516 - t.Errorf("Formatted view should contain title\nGot: %s", result) 516 + t.Error("Markdown should contain title") 517 517 } 518 518 if !strings.Contains(result, "published") { 519 - t.Errorf("Formatted view should contain status 'published'\nGot: %s", result) 519 + t.Error("Markdown should contain status 'published'") 520 520 } 521 521 if !strings.Contains(result, "2024-01-15") { 522 - t.Errorf("Formatted view should contain published date\nGot: %s", result) 522 + t.Error("Markdown should contain published date") 523 523 } 524 - if !strings.Contains(result, "Modified") && !strings.Contains(result, "2024-01-16") { 525 - t.Errorf("Formatted view should contain modified date\nGot: %s", result) 526 - } 527 - if !strings.Contains(result, "test-rkey") { 528 - t.Error("Formatted view should contain rkey") 529 - } 530 - if !strings.Contains(result, "test-cid") { 531 - t.Error("Formatted view should contain cid") 524 + if !strings.Contains(result, "2024-01-16") { 525 + t.Error("Markdown should contain modified date") 532 526 } 533 527 }) 534 528 ··· 541 535 Modified: time.Date(2024, 1, 20, 14, 0, 0, 0, time.UTC), 542 536 } 543 537 544 - result := formatPublicationForView(note) 538 + result := buildPublicationMarkdown(note) 545 539 546 540 if !strings.Contains(result, "Draft Article") { 547 - t.Error("Formatted view should contain title") 541 + t.Error("Markdown should contain title") 548 542 } 549 543 if !strings.Contains(result, "draft") { 550 - t.Error("Formatted view should contain status 'draft'") 544 + t.Error("Markdown should contain status 'draft'") 551 545 } 552 546 if strings.Contains(result, "Published:") { 553 - t.Error("Formatted draft view should not contain published date") 547 + t.Error("Draft markdown should not contain published date") 554 548 } 555 549 if !strings.Contains(result, "2024-01-20 14:00") { 556 - t.Error("Formatted view should contain modified date") 550 + t.Error("Markdown should contain modified date") 557 551 } 558 552 }) 559 553 ··· 566 560 Modified: time.Now(), 567 561 } 568 562 569 - result := formatPublicationForView(note) 563 + result := buildPublicationMarkdown(note) 570 564 571 565 if !strings.Contains(result, "Plain Content") { 572 - t.Error("Formatted view should contain title") 566 + t.Error("Markdown should contain title") 573 567 } 574 568 if !strings.Contains(result, "This content has no markdown header") { 575 - t.Error("Formatted view should contain full content") 569 + t.Error("Markdown should contain full content") 576 570 } 577 571 }) 578 572 ··· 585 579 Modified: time.Now(), 586 580 } 587 581 588 - result := formatPublicationForView(note) 582 + result := buildPublicationMarkdown(note) 589 583 590 584 titleCount := strings.Count(result, "Article Title") 591 585 if titleCount < 1 { 592 - t.Error("Formatted view should contain title at least once") 586 + t.Error("Markdown should contain title at least once") 593 587 } 594 588 if !strings.Contains(result, "Content after title") { 595 - t.Error("Formatted view should contain content after title") 589 + t.Error("Markdown should contain content after title") 596 590 } 597 591 }) 598 592 })
+50 -45
internal/ui/publication_view.go
··· 5 5 "fmt" 6 6 "io" 7 7 "os" 8 - "strings" 8 + "unicode/utf8" 9 9 10 10 "github.com/charmbracelet/bubbles/help" 11 11 "github.com/charmbracelet/bubbles/key" ··· 162 162 status = "draft" 163 163 } 164 164 165 - title := TitleColorStyle.Render(fmt.Sprintf("%s (%s)", m.note.Title, status)) 165 + title := TableTitleStyle.Render(fmt.Sprintf("%s (%s)", m.note.Title, status)) 166 166 content := m.viewport.View() 167 - help := lipgloss.NewStyle().Foreground(lipgloss.Color(Squid.Hex())).Render(m.help.View(m.keys)) 167 + help := MutedStyle.Render(m.help.View(m.keys)) 168 168 169 169 if !m.ready { 170 170 return "\n Initializing..." ··· 173 173 return lipgloss.JoinVertical(lipgloss.Left, title, "", content, "", help) 174 174 } 175 175 176 - // buildPublicationMarkdown creates the markdown content for rendering 177 - func buildPublicationMarkdown(note *models.Note) string { 178 - var content strings.Builder 179 - 180 - content.WriteString("# " + note.Title + "\n\n") 181 - 182 - status := "published" 183 - if note.IsDraft { 184 - status = "draft" 185 - } 186 - content.WriteString("**Status:** " + status + "\n") 187 - 188 - if note.PublishedAt != nil { 189 - content.WriteString("**Published:** " + note.PublishedAt.Format("2006-01-02 15:04") + "\n") 190 - } 191 - 192 - content.WriteString("**Modified:** " + note.Modified.Format("2006-01-02 15:04") + "\n") 193 - 194 - if note.LeafletRKey != nil { 195 - content.WriteString("**RKey:** `" + *note.LeafletRKey + "`\n") 196 - } 197 - 198 - if note.LeafletCID != nil { 199 - content.WriteString("**CID:** `" + *note.LeafletCID + "`\n") 200 - } 201 - 202 - content.WriteString("\n---\n\n") 203 - 204 - noteContent := strings.TrimSpace(note.Content) 205 - if !strings.HasPrefix(noteContent, "# ") { 206 - content.WriteString(noteContent) 207 - } else { 208 - lines := strings.Split(noteContent, "\n") 209 - if len(lines) > 1 { 210 - content.WriteString(strings.Join(lines[1:], "\n")) 211 - } 212 - } 213 - 214 - return content.String() 215 - } 216 - 217 176 // formatPublicationContent renders markdown with glamour for viewport display 218 177 func formatPublicationContent(note *models.Note) (string, error) { 219 178 markdown := buildPublicationMarkdown(note) 220 179 221 180 renderer, err := glamour.NewTermRenderer( 222 181 glamour.WithAutoStyle(), 223 - glamour.WithWordWrap(80), 182 + glamour.WithStandardStyle("tokyo-night"), 183 + glamour.WithPreservedNewLines(), 184 + glamour.WithWordWrap(79), 224 185 ) 225 186 if err != nil { 226 187 return markdown, fmt.Errorf("failed to create renderer: %w", err) ··· 277 238 fmt.Fprint(pv.opts.Output, content) 278 239 return nil 279 240 } 241 + 242 + // ObfuscateMiddle returns a string where the middle portion is replaced by "..." 243 + // TODO: move to package utils or shared 244 + func ObfuscateMiddle(s string, left, right int) string { 245 + if s == "" { 246 + return s 247 + } 248 + if left < 0 { 249 + left = 0 250 + } 251 + if right < 0 { 252 + right = 0 253 + } 254 + 255 + n := utf8.RuneCountInString(s) 256 + if left+right >= n { 257 + return s 258 + } 259 + 260 + var ( 261 + prefixRunes = make([]rune, 0, left) 262 + suffixRunes = make([]rune, 0, right) 263 + ) 264 + i := 0 265 + for _, r := range s { 266 + if i >= left { 267 + break 268 + } 269 + prefixRunes = append(prefixRunes, r) 270 + i++ 271 + } 272 + 273 + if right > 0 { 274 + allRunes := []rune(s) 275 + start := max(n-right, 0) 276 + suffixRunes = append(suffixRunes, allRunes[start:]...) 277 + } 278 + 279 + const repl = "..." 280 + if right == 0 { 281 + return string(prefixRunes) + repl 282 + } 283 + return string(prefixRunes) + repl + string(suffixRunes) 284 + }
+11 -11
internal/ui/publication_view_test.go
··· 138 138 139 139 output := buf.String() 140 140 141 - if !strings.Contains(output, "Test Publication") { 141 + if !strings.Contains(output, "Test") || !strings.Contains(output, "Publication") { 142 142 t.Error("Note title not displayed") 143 143 } 144 144 if !strings.Contains(output, "published") { ··· 153 153 if !strings.Contains(output, "RKey:") { 154 154 t.Error("RKey not displayed") 155 155 } 156 - if !strings.Contains(output, "test-rkey-123") { 156 + if !strings.Contains(output, "tes") || !strings.Contains(output, "123") { 157 157 t.Error("RKey value not displayed") 158 158 } 159 159 if !strings.Contains(output, "CID:") { 160 160 t.Error("CID not displayed") 161 161 } 162 - if !strings.Contains(output, "test-cid-456") { 162 + if !strings.Contains(output, "tes") || !strings.Contains(output, "456") { 163 163 t.Error("CID value not displayed") 164 164 } 165 - if !strings.Contains(output, "This is the content") { 165 + if !strings.Contains(output, "This") || !strings.Contains(output, "content") { 166 166 t.Error("Note content not displayed") 167 167 } 168 168 }) ··· 213 213 214 214 output := buf.String() 215 215 216 - if !strings.Contains(output, "Minimal Note") { 216 + if !strings.Contains(output, "Minimal") || !strings.Contains(output, "Note") { 217 217 t.Error("Note title not displayed") 218 218 } 219 - if !strings.Contains(output, "Simple content") { 219 + if !strings.Contains(output, "Simple") || !strings.Contains(output, "content") { 220 220 t.Error("Note content not displayed") 221 221 } 222 222 if !strings.Contains(output, "Modified:") { ··· 235 235 "**Status:** published", 236 236 "**Published:**", 237 237 "**Modified:**", 238 - "**RKey:** `test-rkey-123`", 239 - "**CID:** `test-cid-456`", 238 + "**RKey:**", 239 + "**CID:**", 240 240 "---", 241 241 "This is the content", 242 242 } ··· 308 308 t.Fatalf("formatPublicationContent failed: %v", err) 309 309 } 310 310 311 - if !strings.Contains(content, "Test Publication") { 311 + if !strings.Contains(content, "Test") || !strings.Contains(content, "Publication") { 312 312 t.Error("Formatted content should include note title") 313 313 } 314 314 }) ··· 634 634 t.Error("No output generated") 635 635 } 636 636 637 - if !strings.Contains(output, note.Title) { 637 + if !strings.Contains(output, "Test") || !strings.Contains(output, "Publication") { 638 638 t.Error("Note title not displayed") 639 639 } 640 - if !strings.Contains(output, "This is the content") { 640 + if !strings.Contains(output, "This") || !strings.Contains(output, "content") { 641 641 t.Error("Note content not displayed") 642 642 } 643 643 })
+4 -4
internal/ui/task_edit.go
··· 350 350 351 351 var content strings.Builder 352 352 353 - title := TitleColorStyle.Render("Edit Task") 353 + title := TableTitleStyle.Render("Edit Task") 354 354 content.WriteString(title + "\n\n") 355 355 356 356 for i, field := range m.fields { 357 357 fieldStyle := lipgloss.NewStyle() 358 358 if i == m.currentField && m.mode == fieldNavigation { 359 - fieldStyle = SelectedColorStyle 359 + fieldStyle = TableSelectedStyle 360 360 } 361 361 362 362 switch field { ··· 427 427 for i, status := range statusOptions { 428 428 style := lipgloss.NewStyle() 429 429 if i == m.statusIndex { 430 - style = SelectedColorStyle 430 + style = TableSelectedStyle 431 431 } 432 432 433 433 line := fmt.Sprintf("%s %s", FormatStatusIndicator(status), status) ··· 460 460 for i, priority := range options { 461 461 style := lipgloss.NewStyle() 462 462 if i == m.priorityIndex { 463 - style = SelectedColorStyle 463 + style = TableSelectedStyle 464 464 } 465 465 466 466 var line string
+14 -26
internal/ui/task_information.go
··· 44 44 PriorityNonePattern = "โ˜†โ˜†โ˜†" 45 45 ) 46 46 47 + // Type aliases for status and priority styles (now defined in palette.go) 47 48 var ( 48 - // Gray 49 - TodoStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8")) 50 - // Blue 51 - InProgressStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("12")) 52 - // Red 53 - BlockedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("9")) 54 - // Green 55 - DoneStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("10")) 56 - // Dark Gray 57 - AbandonedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8")) 58 - // Light Gray 59 - PendingStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("7")) 60 - // Green 61 - CompletedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("10")) 62 - // Dark Red 63 - DeletedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("1")) 64 - // Bright Red - highest urgency 65 - PriorityHighStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("196")) 66 - // Yellow - medium urgency 67 - PriorityMediumStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("11")) 68 - // Cyan - low urgency 69 - PriorityLowStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("14")) 70 - // Gray - no priority 71 - PriorityNoneStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8")) 72 - // For legacy A-Z and numeric priorities 73 - PriorityLegacyStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("13")) // Magenta 49 + TodoStyle = StatusTodo 50 + InProgressStyle = StatusInProgress 51 + BlockedStyle = StatusBlocked 52 + DoneStyle = StatusDone 53 + AbandonedStyle = StatusAbandoned 54 + PendingStyle = StatusPending 55 + CompletedStyle = StatusCompleted 56 + DeletedStyle = StatusDeleted 57 + PriorityHighStyle = PriorityHigh 58 + PriorityMediumStyle = PriorityMedium 59 + PriorityLowStyle = PriorityLow 60 + PriorityNoneStyle = PriorityNone 61 + PriorityLegacyStyle = PriorityLegacy 74 62 ) 75 63 76 64 // GetStatusSymbol returns the unicode symbol for a given status
+2 -2
internal/ui/task_view.go
··· 157 157 return m.help.View(m.keys) 158 158 } 159 159 160 - title := TitleColorStyle.Render(fmt.Sprintf("Task %d", m.task.ID)) 160 + title := TableTitleStyle.Render(fmt.Sprintf("Task %d", m.task.ID)) 161 161 content := m.viewport.View() 162 - help := lipgloss.NewStyle().Foreground(lipgloss.Color(Squid.Hex())).Render(m.help.View(m.keys)) 162 + help := MutedStyle.Render(m.help.View(m.keys)) 163 163 164 164 return lipgloss.JoinVertical(lipgloss.Left, title, "", content, "", help) 165 165 }
+3 -6
tools/docgen.go
··· 1 + //go:build !prod 2 + 1 3 package tools 2 4 3 5 import ( ··· 324 326 } 325 327 326 328 for _, cmd := range root.Commands() { 327 - if cmd.Name() == path[0] || contains(cmd.Aliases, path[0]) { 329 + if cmd.Name() == path[0] || slices.Contains(cmd.Aliases, path[0]) { 328 330 if len(path) == 1 { 329 331 return cmd 330 332 } ··· 334 336 335 337 return nil 336 338 } 337 - 338 - // contains checks if a string is in a slice 339 - func contains(slice []string, str string) bool { 340 - return slices.Contains(slice, str) 341 - }
+21
tools/fetch.go
··· 1 + //go:build !prod 2 + 3 + package tools 4 + 5 + import "github.com/spf13/cobra" 6 + 7 + // NewFetchCommand creates a parent command for fetching remote resources 8 + func NewFetchCommand() *cobra.Command { 9 + cmd := &cobra.Command{ 10 + Use: "fetch", 11 + Short: "Fetch remote resources", 12 + Long: `Fetch and synchronize remote resources from GitHub repositories. 13 + 14 + Includes commands for fetching lexicons, schemas, and other data files.`, 15 + } 16 + 17 + cmd.AddCommand(NewGHRepoCommand()) 18 + cmd.AddCommand(NewLexiconsCommand()) 19 + 20 + return cmd 21 + }
+51
tools/lexicon_fetch.go
··· 1 + //go:build !prod 2 + 3 + package tools 4 + 5 + import ( 6 + "context" 7 + 8 + "github.com/spf13/cobra" 9 + ) 10 + 11 + // NewLexiconsCommand creates a command for fetching Leaflet lexicons 12 + func NewLexiconsCommand() *cobra.Command { 13 + var sha string 14 + var output string 15 + 16 + cmd := &cobra.Command{ 17 + Use: "lexicons", 18 + Short: "Fetch Leaflet lexicons from GitHub", 19 + Long: `Fetches Leaflet lexicons from the hyperlink-academy/leaflet repository. 20 + 21 + This is a convenience wrapper around gh-repo with pre-configured defaults 22 + for the Leaflet lexicon repository.`, 23 + Example: ` # Fetch latest lexicons 24 + noteleaf tools fetch lexicons 25 + 26 + # Fetch from a specific commit 27 + noteleaf tools fetch lexicons --sha abc123def 28 + 29 + # Fetch to a custom directory 30 + noteleaf tools fetch lexicons --output ./tmp/lexicons`, 31 + RunE: func(cmd *cobra.Command, args []string) error { 32 + config := ArchiveConfig{ 33 + Repo: "hyperlink-academy/leaflet", 34 + Path: "lexicons/pub/leaflet/", 35 + Output: output, 36 + SHA: sha, 37 + FormatJSON: true, 38 + } 39 + 40 + ctx := cmd.Context() 41 + if ctx == nil { 42 + ctx = context.Background() 43 + } 44 + 45 + return fetchAndExtractArchive(ctx, config, cmd.OutOrStdout()) 46 + }, 47 + } 48 + cmd.Flags().StringVar(&sha, "sha", "", "Specific commit SHA (default: latest)") 49 + cmd.Flags().StringVar(&output, "output", "lexdocs/leaflet/", "Output directory for lexicons") 50 + return cmd 51 + }
+19
tools/registry.go
··· 1 + //go:build !prod 2 + 3 + package tools 4 + 5 + import "github.com/spf13/cobra" 6 + 7 + // NewToolsCommand creates a parent command for all development tools 8 + func NewToolsCommand(root *cobra.Command) *cobra.Command { 9 + cmd := &cobra.Command{ 10 + Use: "tools", 11 + Short: "Development and maintenance tools", 12 + Long: `Development tools for documentation generation, data synchronization, 13 + and maintenance tasks. These commands are only available in dev builds.`, 14 + } 15 + cmd.AddCommand(NewDocGenCommand(root)) 16 + cmd.AddCommand(NewFetchCommand()) 17 + 18 + return cmd 19 + }
+251
tools/repo_archive.go
··· 1 + //go:build !prod 2 + 3 + package tools 4 + 5 + import ( 6 + "archive/tar" 7 + "compress/gzip" 8 + "context" 9 + "encoding/json" 10 + "fmt" 11 + "io" 12 + "net/http" 13 + "os" 14 + "path/filepath" 15 + "strings" 16 + "time" 17 + 18 + "github.com/spf13/cobra" 19 + ) 20 + 21 + // GitHubCommit represents a GitHub API commit response 22 + type GitHubCommit struct { 23 + SHA string `json:"sha"` 24 + Commit struct { 25 + Message string `json:"message"` 26 + } `json:"commit"` 27 + } 28 + 29 + // ArchiveConfig contains configuration for fetching and extracting archives 30 + type ArchiveConfig struct { 31 + Repo string 32 + Path string 33 + Output string 34 + SHA string 35 + FormatJSON bool 36 + } 37 + 38 + // NewGHRepoCommand creates a command for fetching GitHub repository archives 39 + func NewGHRepoCommand() *cobra.Command { 40 + var config ArchiveConfig 41 + 42 + cmd := &cobra.Command{ 43 + Use: "gh-repo", 44 + Short: "Fetch and extract files from a GitHub repository archive", 45 + Long: `Fetches a GitHub repository archive (tarball), extracts specific paths, 46 + and optionally formats JSON files using Go's standard library. 47 + 48 + This is useful for syncing lexicons, schemas, or other data files from GitHub repositories.`, 49 + Example: ` # Fetch lexicons from a specific path 50 + noteleaf tools fetch gh-repo \ 51 + --repo hyperlink-academy/leaflet \ 52 + --path lexicons/pub/leaflet/ \ 53 + --output lexdocs/leaflet/ 54 + 55 + # Fetch from a specific commit 56 + noteleaf tools fetch gh-repo \ 57 + --repo owner/repo \ 58 + --path schemas/ \ 59 + --output local/schemas/ \ 60 + --sha abc123def`, 61 + RunE: func(cmd *cobra.Command, args []string) error { 62 + if config.Repo == "" { 63 + return fmt.Errorf("--repo is required") 64 + } 65 + if config.Path == "" { 66 + return fmt.Errorf("--path is required") 67 + } 68 + if config.Output == "" { 69 + return fmt.Errorf("--output is required") 70 + } 71 + 72 + ctx := cmd.Context() 73 + if ctx == nil { 74 + ctx = context.Background() 75 + } 76 + 77 + return fetchAndExtractArchive(ctx, config, cmd.OutOrStdout()) 78 + }, 79 + } 80 + 81 + cmd.Flags().StringVar(&config.Repo, "repo", "", "GitHub repository (owner/name)") 82 + cmd.Flags().StringVar(&config.Path, "path", "", "Path within repository to extract") 83 + cmd.Flags().StringVar(&config.Output, "output", "", "Output directory for extracted files") 84 + cmd.Flags().StringVar(&config.SHA, "sha", "", "Specific commit SHA (default: latest)") 85 + cmd.Flags().BoolVar(&config.FormatJSON, "format-json", true, "Format JSON files with indentation") 86 + return cmd 87 + } 88 + 89 + // fetchAndExtractArchive fetches a GitHub archive and extracts specific paths 90 + func fetchAndExtractArchive(ctx context.Context, config ArchiveConfig, out io.Writer) error { 91 + sha := config.SHA 92 + if sha == "" { 93 + var err error 94 + sha, err = getLatestCommit(ctx, config.Repo, config.Path) 95 + if err != nil { 96 + return fmt.Errorf("failed to get latest commit: %w", err) 97 + } 98 + fmt.Fprintf(out, "Latest commit: %s\n", sha) 99 + } 100 + 101 + tmpDir, err := os.MkdirTemp("", "repo-archive-*") 102 + if err != nil { 103 + return fmt.Errorf("failed to create temp directory: %w", err) 104 + } 105 + defer os.RemoveAll(tmpDir) 106 + 107 + fmt.Fprintf(out, "Fetching archive for %s@%s\n", config.Repo, sha[:7]) 108 + if err := downloadAndExtract(ctx, config.Repo, sha, config.Path, tmpDir, config.FormatJSON, out); err != nil { 109 + return fmt.Errorf("failed to download and extract: %w", err) 110 + } 111 + 112 + fmt.Fprintf(out, "Writing README with source information\n") 113 + readme := fmt.Sprintf("Source: https://github.com/%s/tree/%s/%s\n", config.Repo, sha, config.Path) 114 + if err := os.WriteFile(filepath.Join(tmpDir, "README.md"), []byte(readme), 0o644); err != nil { 115 + return fmt.Errorf("failed to write README: %w", err) 116 + } 117 + 118 + fmt.Fprintf(out, "Moving extracted files to %s\n", config.Output) 119 + if err := os.RemoveAll(config.Output); err != nil { 120 + return fmt.Errorf("failed to remove existing output directory: %w", err) 121 + } 122 + if err := os.Rename(tmpDir, config.Output); err != nil { 123 + return fmt.Errorf("failed to move files to output directory: %w", err) 124 + } 125 + 126 + fmt.Fprintf(out, "Successfully extracted archive to %s\n", config.Output) 127 + return nil 128 + } 129 + 130 + // getLatestCommit fetches the latest commit SHA for a given repository and path 131 + func getLatestCommit(ctx context.Context, repo, path string) (string, error) { 132 + url := fmt.Sprintf("https://api.github.com/repos/%s/commits?path=%s&per_page=1", repo, path) 133 + 134 + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 135 + if err != nil { 136 + return "", err 137 + } 138 + 139 + client := &http.Client{Timeout: 30 * time.Second} 140 + resp, err := client.Do(req) 141 + if err != nil { 142 + return "", err 143 + } 144 + defer resp.Body.Close() 145 + 146 + if resp.StatusCode != http.StatusOK { 147 + return "", fmt.Errorf("GitHub API returned status %d", resp.StatusCode) 148 + } 149 + 150 + var commits []GitHubCommit 151 + if err := json.NewDecoder(resp.Body).Decode(&commits); err != nil { 152 + return "", fmt.Errorf("failed to decode response: %w", err) 153 + } 154 + 155 + if len(commits) == 0 { 156 + return "", fmt.Errorf("no commits found for path %s", path) 157 + } 158 + 159 + return commits[0].SHA, nil 160 + } 161 + 162 + // downloadAndExtract downloads a GitHub archive and extracts files from a specific path 163 + func downloadAndExtract(ctx context.Context, repo, sha, extractPath, outputDir string, formatJSON bool, out io.Writer) error { 164 + url := fmt.Sprintf("https://github.com/%s/archive/%s.tar.gz", repo, sha) 165 + 166 + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 167 + if err != nil { 168 + return err 169 + } 170 + 171 + client := &http.Client{Timeout: 5 * time.Minute} 172 + resp, err := client.Do(req) 173 + if err != nil { 174 + return err 175 + } 176 + defer resp.Body.Close() 177 + 178 + if resp.StatusCode != http.StatusOK { 179 + return fmt.Errorf("failed to download archive: status %d", resp.StatusCode) 180 + } 181 + 182 + gzr, err := gzip.NewReader(resp.Body) 183 + if err != nil { 184 + return fmt.Errorf("failed to create gzip reader: %w", err) 185 + } 186 + defer gzr.Close() 187 + 188 + tr := tar.NewReader(gzr) 189 + 190 + repoName := strings.Split(repo, "/")[1] 191 + prefix := fmt.Sprintf("%s-%s/%s", repoName, sha, extractPath) 192 + 193 + fmt.Fprintf(out, "Extracting files from %s\n", prefix) 194 + 195 + fileCount := 0 196 + for { 197 + header, err := tr.Next() 198 + if err == io.EOF { 199 + break 200 + } 201 + if err != nil { 202 + return fmt.Errorf("failed to read tar header: %w", err) 203 + } 204 + 205 + if header.Typeflag != tar.TypeReg { 206 + continue 207 + } 208 + 209 + if !strings.HasPrefix(header.Name, prefix) { 210 + continue 211 + } 212 + 213 + if !strings.HasSuffix(header.Name, ".json") { 214 + continue 215 + } 216 + 217 + relativePath := strings.TrimPrefix(header.Name, prefix) 218 + outputPath := filepath.Join(outputDir, relativePath) 219 + 220 + if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil { 221 + return fmt.Errorf("failed to create directory for %s: %w", outputPath, err) 222 + } 223 + 224 + data, err := io.ReadAll(tr) 225 + if err != nil { 226 + return fmt.Errorf("failed to read file %s: %w", header.Name, err) 227 + } 228 + 229 + if formatJSON { 230 + var jsonData any 231 + if err := json.Unmarshal(data, &jsonData); err != nil { 232 + return fmt.Errorf("failed to parse JSON in %s: %w", header.Name, err) 233 + } 234 + 235 + formattedData, err := json.MarshalIndent(jsonData, "", " ") 236 + if err != nil { 237 + return fmt.Errorf("failed to format JSON in %s: %w", header.Name, err) 238 + } 239 + data = append(formattedData, '\n') 240 + } 241 + 242 + if err := os.WriteFile(outputPath, data, 0o644); err != nil { 243 + return fmt.Errorf("failed to write file %s: %w", outputPath, err) 244 + } 245 + 246 + fileCount++ 247 + } 248 + 249 + fmt.Fprintf(out, "Extracted %d files\n", fileCount) 250 + return nil 251 + }
+68
website/TODO.md
··· 1 + # Noteleaf Documentation TODO 2 + 3 + This document tracks documentation coverage for the Noteleaf website. The goal is to provide comprehensive documentation for both the productivity system features and the leaflet.pub publishing capabilities. 4 + 5 + ## Integration and Workflows 6 + 7 + - [x] External Integrations 8 + - [x] Open Library API 9 + - [x] Leaflet.pub API 10 + - [x] ATProto authentication 11 + - [ ] Workflows and Examples 12 + - [ ] Daily task review workflow 13 + - [ ] Note-taking for research 14 + - [ ] Reading list management 15 + - [ ] Publishing a blog post to leaflet.pub 16 + - [ ] Linking tasks, notes, and media 17 + - [x] Import/Export 18 + - [x] Exporting data 19 + - [x] Backup and restore 20 + - [x] Migration from other tools 21 + 22 + ## Development 23 + 24 + - [x] Building Noteleaf 25 + - [x] Development vs production builds 26 + - [x] Build tags 27 + - [x] Task automation (Taskfile) 28 + - [x] Testing 29 + - [x] Running tests 30 + - [x] Coverage reports 31 + - [x] Test patterns and scaffolding 32 + - [ ] Contributing 33 + - [ ] Code organization 34 + - [ ] Adding new commands 35 + - [ ] UI components 36 + - [ ] Testing requirements 37 + - [ ] Architecture Deep Dive 38 + - [ ] Repository pattern 39 + - [ ] Handler architecture 40 + - [ ] Service layer 41 + - [ ] Data models 42 + - [ ] UI component system 43 + 44 + ## Examples and Tutorials 45 + 46 + - [ ] Getting Started Tutorial 47 + - [ ] First 15 minutes 48 + - [ ] Essential workflows 49 + - [ ] Task Management Tutorials 50 + - [ ] GTD workflow 51 + - [ ] Time blocking 52 + - [ ] Project planning 53 + - [ ] Note-Taking Tutorials 54 + - [ ] Zettelkasten method 55 + - [ ] Research notes 56 + - [ ] Meeting notes 57 + 58 + ## Appendices 59 + 60 + - [ ] Glossary 61 + - [ ] Keyboard Shortcuts Reference 62 + - [ ] Configuration Options Reference 63 + - [ ] API Reference (leaflet schema) 64 + - [ ] Color Palette Reference 65 + - [ ] Migration Guides 66 + - [ ] From TaskWarrior 67 + - [ ] From todo.txt 68 + - [ ] From other note-taking apps
+101 -8
website/docs/Configuration.md
··· 1 + --- 2 + id: configuration 3 + title: Configuration 4 + sidebar_label: Configuration 5 + sidebar_position: 6 6 + description: Reference for configuration locations, defaults, and options. 7 + --- 8 + 1 9 # Configuration 2 10 3 11 Noteleaf stores its configuration in a TOML file. The configuration file location depends on your operating system and can be overridden with environment variables. ··· 31 39 ```sh 32 40 export NOTELEAF_CONFIG=/path/to/custom/config.toml 33 41 ``` 42 + 43 + ### File Structure 44 + 45 + Configuration is stored as [TOML](https://toml.io). Each key maps 1:1 with the fields shown by `noteleaf config show`. A minimal file looks like: 46 + 47 + ```toml 48 + date_format = "2006-01-02" 49 + color_scheme = "default" 50 + ``` 51 + 52 + There is no required orderingโ€”the parser loads the file, applies defaults for missing keys, and writes a normalized version whenever you run `noteleaf config set ...`. This means you can edit the file manually or stick entirely to CLI helpers. 34 53 35 54 ## Configuration Options 36 55 ··· 318 337 **Type:** String (ISO8601) 319 338 **Default:** None 320 339 340 + ## Editor Integration 341 + 342 + The `editor` key wires Noteleaf into your preferred text editor. Resolution order: 343 + 344 + 1. `editor` inside `.noteleaf.conf.toml` 345 + 2. `$EDITOR` environment variable 346 + 3. System default (usually `vi` on Unix) 347 + 348 + Where it is used: 349 + 350 + - `noteleaf note edit <id>` always opens the configured editor. 351 + - `noteleaf note create -e` or `--editor` lets you capture inline text and immediately refine it in the editor. 352 + - Interactive creation (`noteleaf note create -i`) respects the same setting when you choose to open the note. 353 + 354 + Example configuration: 355 + 356 + ```toml 357 + editor = "nvim" 358 + ``` 359 + 360 + If you frequently switch editors, leave the config empty and export `$EDITOR` before launching Noteleaf: 361 + 362 + ```sh 363 + EDITOR="zed" noteleaf note edit 5 364 + ``` 365 + 321 366 ## Managing Configuration 322 367 323 368 ### View Current Configuration ··· 389 434 390 435 ## Environment Variables 391 436 392 - Noteleaf respects the following environment variables: 437 + Environment overrides are resolved before configuration values. Set them when you need temporary behavior (CI jobs, alternate workspaces, etc.). 393 438 394 - - `NOTELEAF_CONFIG` - Custom path to configuration file 395 - - `NOTELEAF_DATA_DIR` - Custom path to data directory 396 - - `EDITOR` - Default text editor (fallback if `editor` config not set) 439 + | Variable | Purpose | Notes | 440 + |----------|---------|-------| 441 + | `NOTELEAF_CONFIG` | Absolute path to the TOML file | Overrides platform defaults. Parent directories are created automatically. | 442 + | `NOTELEAF_DATA_DIR` | Root directory for the SQLite DB, notes, articles, and attachments | Useful for portable installs (USB drive, synced folder). | 443 + | `EDITOR` | Fallback editor when the `editor` config key is empty | Checked by all note-related commands. | 397 444 398 - Example: 445 + Usage example: 399 446 400 447 ```sh 401 - export NOTELEAF_CONFIG=~/.config/noteleaf/config.toml 402 - export NOTELEAF_DATA_DIR=~/Documents/noteleaf-data 403 - export EDITOR=nvim 448 + export NOTELEAF_CONFIG=~/.config/noteleaf/work.conf.toml 449 + export NOTELEAF_DATA_DIR=~/Sync/workspace-data 450 + export EDITOR=helix 404 451 ``` 452 + 453 + Because `NOTELEAF_DATA_DIR` cascades to the article and note directories, a single export is all you need to relocate the entire knowledge base. 454 + 455 + ## Customization 456 + 457 + ### Themes and Colors 458 + 459 + The `color_scheme` option controls how Fang (the underlying Cobra replacement) styles command help and certain UI components. Right now the only valid value is `"default"`, which maps to Noteleafโ€™s Iceberg-inspired palette. Future releases will add explicit `light`/`dark` options; until then customization requires overriding your terminal theme or building Noteleaf from source with changes in `internal/ui/palette.go`. 460 + 461 + ```toml 462 + color_scheme = "default" # leave blank to adopt upcoming auto-mode 463 + ``` 464 + 465 + ### Keyboard Shortcuts 466 + 467 + All interactive views share the same key map: 468 + 469 + | Keys | Action | 470 + |------|--------| 471 + | `โ†‘ / k`, `โ†“ / j` | Move selection | 472 + | `enter` | Open the selected row | 473 + | `v` | View details in a side panel (where supported) | 474 + | `/` | Search/filter (live) | 475 + | `r` | Reload data | 476 + | `?` | Show full help, including custom actions for the current view | 477 + | `q`, `ctrl+c` | Quit the view | 478 + | `esc`, `backspace` | Exit search/help/detail panels | 479 + | `1-9` | Jump directly to the corresponding row index | 480 + 481 + Press `?` inside any list/table to confirm the bindingsโ€”this uses Bubble Teaโ€™s built-in help component so it always reflects the current screen. 482 + 483 + ### Output Formats 484 + 485 + - `export_format` sets the default for future export commands (currently `json`). 486 + - Task commands support JSON today: `noteleaf todo view 12 --json` or `noteleaf todo list --static --json`. 487 + - The `--format` flag on `noteleaf todo view` switches between `detailed` and `brief` layouts, which is handy when scripting. 488 + 489 + Examples: 490 + 491 + ```sh 492 + noteleaf todo view 12 --format brief --json | jq '.status' 493 + noteleaf todo list --static --json > tasks.json 494 + noteleaf config set export_format "csv" # prepare for upcoming exporters 495 + ``` 496 + 497 + Even when there is no dedicated exporter yet, the SQLite database lives in the open, so you can always run your own `SELECT ...` queries or use `sqlite-utils` to produce CSV/JSON.
+8
website/docs/Quickstart.md
··· 1 + --- 2 + id: quickstart 3 + title: Quickstart Guide 4 + sidebar_label: Quickstart 5 + sidebar_position: 1 6 + description: Install Noteleaf and learn the essentials in minutes. 7 + --- 8 + 1 9 # Quickstart Guide 2 10 3 11 This guide will walk you through installing Noteleaf and getting productive with tasks, notes, and media tracking in under 15 minutes.
+9
website/docs/articles/_category_.json
··· 1 + { 2 + "label": "Articles", 3 + "position": 6, 4 + "link": { 5 + "type": "generated-index", 6 + "title": "Article Archive", 7 + "description": "Save, read, and organize web articles locally." 8 + } 9 + }
+75
website/docs/articles/management.md
··· 1 + --- 2 + title: Article Management 3 + sidebar_label: Management 4 + description: Save URLs, inspect metadata, and read articles without leaving the CLI. 5 + sidebar_position: 2 6 + --- 7 + 8 + # Article Management 9 + 10 + ## Save Articles from URLs 11 + 12 + ```sh 13 + noteleaf article add https://example.com/long-form-piece 14 + ``` 15 + 16 + What happens: 17 + 18 + 1. The CLI checks the database to ensure the URL was not imported already. 19 + 2. It fetches the page with a reader-friendly User-Agent (`curl/8.4.0`) and English `Accept-Language` headers to avoid blocked responses. 20 + 3. Parsed content is written to Markdown and HTML files under `articles_dir`. 21 + 4. A database record is inserted with all metadata and file paths. 22 + 23 + If parsing fails (unsupported domain, network issue, etc.) nothing is written to disk, so partial entries never appear in your archive. 24 + 25 + ## Parsing and Extraction 26 + 27 + The parser uses a two-layer strategy: 28 + 29 + 1. **Domain-specific rules** check the XPath selectors defined in `internal/articles/rules`. These rules strip unwanted elements (cookie banners, nav bars), capture the main body, and record author/date fields accurately. 30 + 2. **Heuristic fallback** scores every DOM node, penalizes high link-density sections, and picks the most โ€œarticle-likeโ€ block. It also pulls metadata from JSON-LD `Article` objects when available. 31 + 32 + During saving, the Markdown file gets a generated header: 33 + 34 + ```markdown 35 + # Article Title 36 + 37 + **Author:** Jane Smith 38 + 39 + **Date:** 2024-01-02 40 + 41 + **Source:** https://example.com/long-form-piece 42 + 43 + **Saved:** 2024-02-05 10:45:12 44 + ``` 45 + 46 + Everything after the `---` separator is the cleaned article content. 47 + 48 + ## Reading in the Terminal 49 + 50 + There are two ways to inspect what you saved: 51 + 52 + - `noteleaf article view <id>` shows metadata, verifies whether the files still exist, and prints the first ~20 lines as a preview. 53 + - `noteleaf article read <id>` renders the full Markdown using [Charmโ€™s Glamour](https://github.com/charmbracelet/glamour), giving you syntax highlighting, proper headings, and wrapped paragraphs directly in the terminal. 54 + 55 + If you prefer your editor, open the Markdown path printed by `view`. Both Markdown and HTML copies belong to you, so feel free to annotate or reformat them. 56 + 57 + ## Article Metadata Reference 58 + 59 + Use `noteleaf article list` to see titles and authors: 60 + 61 + ```sh 62 + noteleaf article list # newest first 63 + noteleaf article list "sqlite" # full-text filter on titles 64 + noteleaf article list --author "Kim" # author filter 65 + noteleaf article list -l 5 # top 5 results 66 + ``` 67 + 68 + Each entry includes the created timestamp. The `view` command provides the raw paths so you can script around them, for example: 69 + 70 + ```sh 71 + md=$(noteleaf article view 12 | rg 'Markdown:' | awk '{print $3}') 72 + $EDITOR "$md" 73 + ``` 74 + 75 + All metadata lives in the SQLite `articles` table, making it easy to run your own reports with `sqlite3` if needed.
+71
website/docs/articles/organization.md
··· 1 + --- 2 + title: Article Organization 3 + sidebar_label: Organization 4 + description: Filter your archive, add lightweight tags, and keep backups tidy. 5 + sidebar_position: 3 6 + --- 7 + 8 + # Article Organization 9 + 10 + ## Filter by Author or Title 11 + 12 + `noteleaf article list` accepts both a free-form query (matches the title) and dedicated flags: 13 + 14 + ```sh 15 + # Anything with "SQLite" in the title 16 + noteleaf article list SQLite 17 + 18 + # Limit to a single author 19 + noteleaf article list --author "Ada Palmer" 20 + 21 + # Cap the output for quick reviews 22 + noteleaf article list --author "Ada Palmer" --limit 3 23 + ``` 24 + 25 + Because the database stores created timestamps, results come back with the newest article first, making it easy to run weekly reviews. 26 + 27 + ## Tagging Articles 28 + 29 + There is no first-class tagging UI yet, but Markdown files are yours to edit. Common patterns: 30 + 31 + ```markdown 32 + --- 33 + tags: [distributed-systems, reference] 34 + project: moonshot 35 + --- 36 + ``` 37 + 38 + Drop that block right after the generated metadata and tools like `rg` or `ripgrep --json` can surface tagged snippets instantly. You can also maintain a separate note that lists article IDs per topic if you prefer not to edit the captured files. 39 + 40 + ## Read vs Unread 41 + 42 + Opening an article in the terminal does not flip a status flag. Use one of these lightweight conventions instead: 43 + 44 + - Prefix the Markdown filename with `read-` once you are done. 45 + - Keep a running checklist note (e.g., โ€œArticles Inboxโ€) that references IDs and mark them off as you read them. 46 + - Create a task linked to the article ID (`todo add "Summarize article #14"`), then close the task when you finish. 47 + 48 + All three approaches work today and will map cleanly to future built-in read/unread tracking. 49 + 50 + ## Archiving and Backups 51 + 52 + The archive lives under `articles_dir`. By default that is `<data_dir>/articles`, where `<data_dir>` depends on your OS: 53 + 54 + | Platform | Default | 55 + |----------|---------| 56 + | Linux | `~/.local/share/noteleaf/articles` | 57 + | macOS | `~/Library/Application Support/noteleaf/articles` | 58 + | Windows | `%LOCALAPPDATA%\noteleaf\articles` | 59 + 60 + You can override the location via the `articles_dir` setting in `~/.config/noteleaf/.noteleaf.conf.toml` (or by pointing `NOTELEAF_DATA_DIR` to a different root before launching the CLI). 61 + 62 + Because every import produces Markdown + HTML, the directory is perfect for version control: 63 + 64 + ```sh 65 + cd ~/.local/share/noteleaf/articles 66 + git init 67 + git add . 68 + git commit -m "Initial snapshot of article archive" 69 + ``` 70 + 71 + Pair that with your cloud backup tool of choice and you have a durable, fully-offline knowledge base that still integrates seamlessly with Noteleafโ€™s search commands.
+54
website/docs/articles/overview.md
··· 1 + --- 2 + title: Article Overview 3 + sidebar_label: Overview 4 + description: How the article parser saves content for offline reading. 5 + sidebar_position: 1 6 + --- 7 + 8 + # Article Overview 9 + 10 + The `noteleaf article` command turns any supported URL into two files on disk: 11 + 12 + - A clean Markdown document (great for terminal reading). 13 + - A rendered HTML copy (handy for rich export or sharing). 14 + 15 + Both files live inside your configured `articles_dir` (defaults to `<data_dir>/articles`). The SQLite database stores the metadata and file paths so you can query, list, and delete articles without worrying about directories. 16 + 17 + ## How Parsing Works 18 + 19 + 1. **Domain rules first**: Each supported site has a small XPath rule file (`internal/articles/rules/*.txt`). 20 + 2. **Heuristic fallback**: When no rule exists, the parser falls back to the readability-style heuristic extractor that scores DOM nodes, removes nav bars, and preserves headings/links. 21 + 3. **Metadata extraction**: The parser also looks for OpenGraph/JSON-LD tags to capture author names and publish dates. 22 + 23 + You can see the currently loaded rule set by running: 24 + 25 + ```sh 26 + noteleaf article --help 27 + ``` 28 + 29 + The help output prints the supported domains and the storage directory that is currently in use. 30 + 31 + ## Saved Metadata 32 + 33 + Every article record contains: 34 + 35 + - URL and canonical title 36 + - Author (if present in metadata) 37 + - Publication date (stored as plain text, e.g., `2024-01-02`) 38 + - Markdown file path 39 + - HTML file path 40 + - Created/modified timestamps 41 + 42 + These fields make it easy to build reading logs, cite sources in notes, or reference articles from tasks. 43 + 44 + ## Commands at a Glance 45 + 46 + | Command | Purpose | 47 + |----------------------------------|---------| 48 + | `noteleaf article add <url>` | Parse, save, and index a URL | 49 + | `noteleaf article list [query]` | Show saved items; filter with `--author` or `--limit` | 50 + | `noteleaf article view <id>` | Inspect metadata + a short preview | 51 + | `noteleaf article read <id>` | Render the Markdown nicely in your terminal | 52 + | `noteleaf article remove <id>` | Delete the DB entry and the files | 53 + 54 + The CLI automatically prevents duplicate imports by checking the URL before parsing.
+9
website/docs/concepts/_category_.json
··· 1 + { 2 + "label": "Concepts", 3 + "position": 2, 4 + "link": { 5 + "type": "generated-index", 6 + "title": "Concepts", 7 + "description": "Core ideas, architecture, and design principles behind Noteleaf." 8 + } 9 + }
+44
website/docs/concepts/architecture.md
··· 1 + --- 2 + title: Architecture Overview 3 + sidebar_label: Architecture 4 + description: Application structure, storage, and UI layers. 5 + sidebar_position: 3 6 + --- 7 + 8 + # Architecture Overview 9 + 10 + ## Architecture Overview 11 + 12 + ### Application Structure 13 + 14 + Noteleaf follows a clean architecture pattern with clear separation of concerns: 15 + 16 + ``` 17 + cmd/ - CLI commands and user interface 18 + internal/ 19 + handlers/ - Business logic and orchestration 20 + repo/ - Database access layer 21 + ui/ - Terminal UI components (Bubble Tea) 22 + models/ - Domain models 23 + public/ - Leaflet.pub integration 24 + ``` 25 + 26 + Each layer has defined responsibilities with minimal coupling between them. 27 + 28 + ### Storage and Database 29 + 30 + **SQLite Database**: All structured data (tasks, metadata, relationships) lives in a single SQLite file at `~/.local/share/noteleaf/noteleaf.db` (Linux) or `~/Library/Application Support/noteleaf/noteleaf.db` (macOS). 31 + 32 + **Markdown Files**: Note content is stored as individual markdown files on disk. The database tracks metadata while keeping your notes in a portable, human-readable format. 33 + 34 + **Database Schema**: Tables for tasks, notes, articles, books, movies, TV shows, publications, and linking tables for tags and relationships. Migrations handle schema evolution. 35 + 36 + ### TUI Framework (Bubble Tea) 37 + 38 + The interactive interface is built with [Bubble Tea](https://github.com/charmbracelet/bubbletea), a Go framework for terminal user interfaces based on The Elm Architecture: 39 + 40 + - **Model**: Application state (current view, selected item, filters) 41 + - **Update**: State transitions based on user input 42 + - **View**: Render the current state to the terminal 43 + 44 + This architecture makes the UI predictable, testable, and composable. Each screen is an independent component that can be developed and tested in isolation.
+119
website/docs/concepts/cli-reference.md
··· 1 + --- 2 + title: CLI Reference 3 + sidebar_label: CLI Reference 4 + description: Overview of Noteleafโ€™s command hierarchy, flags, and developer utilities. 5 + sidebar_position: 7 6 + --- 7 + 8 + # CLI Reference 9 + 10 + This reference is a map of the top-level commands exposed by `noteleaf`. For flag-by-flag detail run `noteleaf <command> --help`โ€”the human-friendly Fang help screens are always the source of truth. 11 + 12 + ## Command Structure 13 + 14 + ### Global flags 15 + 16 + | Flag | Description | 17 + | ------------------------- | ----------------------------------------------------------------- | 18 + | `--help`, `-h` | Show help for any command or subcommand | 19 + | `--version` | Print the Noteleaf build string (includes git SHA when available) | 20 + | `--color <auto\|on\|off>` | Optional Fang flag to control ANSI colors | 21 + 22 + Environment variables such as `NOTELEAF_CONFIG`, `NOTELEAF_DATA_DIR`, and `EDITOR` affect how commands behave but are not flags. 23 + 24 + ### Command hierarchy 25 + 26 + - Root command: `noteleaf` 27 + - Task commands live under the `todo` alias (e.g., `noteleaf todo add`). 28 + - Media commands are grouped and require a subtype: `noteleaf media book`, `noteleaf media movie`, `noteleaf media tv`. 29 + - Publishing flows live under `noteleaf pub`. 30 + - Management helpers (`config`, `setup`, `status`, `reset`) sit at the top level. 31 + 32 + ### Help system 33 + 34 + Every command inherits Fangโ€™s colorized help plus Noteleaf-specific additions: 35 + 36 + - `noteleaf article --help` prints the supported parser domains and storage directory by calling into the handler. 37 + - Interactive commands show the keyboard shortcuts inside their help output. 38 + - You can always drill down: `noteleaf todo add --help`, `noteleaf media book list --help`, etc. 39 + 40 + ## Commands by Category 41 + 42 + ### `todo` / `task` 43 + 44 + Add, list, view, update, complete, and annotate tasks. Supports priorities, contexts, tags, dependencies, recurrence, and JSON output for scripting. Related metadata commands (`projects`, `tags`, `contexts`) summarize usage counts. 45 + 46 + ### `note` 47 + 48 + Create Markdown notes (inline, from files, or via the interactive editor), list them with the TUI, search, view, edit in `$EDITOR`, archive/unarchive, and delete. Notes share IDs with leaflet publishing so they can be synced later. 49 + 50 + ### `media` 51 + 52 + Umbrella group for personal queues: 53 + 54 + - `noteleaf media book` โ€” Search Open Library, add books, update status (`queued`/`reading`/`finished`), edit progress percentages, and remove titles. 55 + - `noteleaf media movie` โ€” Search Rotten Tomatoes, queue movies, mark them watched, or remove them. 56 + - `noteleaf media tv` โ€” Same as movies but with watching/watched states and optional season/episode tracking. 57 + 58 + Each subtype has its own `list`, status-changing verbs, and removal commands. Use `-i/--interactive` on `add` to open the TUI selector (books today, other media soon). 59 + 60 + ### `article` 61 + 62 + Parse and save web articles with `add <url>`, inspect them via `list`, `view`, or `read`, and delete them with `remove`. All commands operate on the local Markdown/HTML archive referenced in the handler output. 63 + 64 + ### `pub` 65 + 66 + Leaflet.pub commands for AT Protocol publishing: 67 + 68 + - `pull` / `push` to sync notes with the remote publication. 69 + - `status`, `list`, and `diff` to inspect what is linked. 70 + - Support for working drafts, batch pushes, and file-based imports (`--file`) when publishing is combined with local markdown. 71 + 72 + ### `config` 73 + 74 + Inspect and mutate `~/.noteleaf.conf.toml`: 75 + 76 + - `noteleaf config show` (or `get <key>`) prints values. 77 + - `noteleaf config set <key> <value>` writes back to disk. 78 + - `noteleaf config path` reveals the file location. 79 + - `noteleaf config reset` rewinds to defaults. 80 + 81 + ### `setup` 82 + 83 + `noteleaf setup` initializes the database, config file, and data directories if they do not exist. `noteleaf setup seed` can load sample data (pass `--force` to wipe existing rows first). 84 + 85 + ### `status` 86 + 87 + `noteleaf status` prints absolute paths for the config file, data directory, database, and media folders along with environment overridesโ€”handy for debugging or verifying a portable install. 88 + 89 + ## Development Tools 90 + 91 + `noteleaf tools ...` is available in development builds (`task build:dev`, `go run ./cmd`). It bundles maintenance utilities: 92 + 93 + ### Documentation generation 94 + 95 + ``` 96 + noteleaf tools docgen --format docusaurus --out website/docs/manual 97 + noteleaf tools docgen --format man --out docs/manual 98 + ``` 99 + 100 + Generates reference docs straight from the command definitions, keeping terminal help and published docs in sync. 101 + 102 + ### Lexicon fetching 103 + 104 + ``` 105 + noteleaf tools fetch lexicons 106 + noteleaf tools fetch lexicons --sha <commit> 107 + ``` 108 + 109 + Pulls the latest `leaflet.pub` lexicons from GitHub so the AT Protocol client stays current. You can point it at a specific commit for reproducible builds. 110 + 111 + ### Database utilities 112 + 113 + ``` 114 + noteleaf tools fetch gh-repo --repo owner/repo --path schemas --output tmp/schemas 115 + ``` 116 + 117 + Provides generic fetchers plus helpers used by CI and local testing to refresh schema files, warm caches, or introspect the SQLite database. 118 + 119 + These tools intentionally live behind the dev build tag so production binaries stay lean. Use them when contributing documentation or publishing features.
+19
website/docs/concepts/data-model.md
··· 1 + --- 2 + title: Unified Data Model 3 + sidebar_label: Data Model 4 + description: How tasks, notes, and media connect inside Noteleaf. 5 + sidebar_position: 2 6 + --- 7 + 8 + # Unified Data Model 9 + 10 + ## Philosophy 11 + 12 + Noteleaf treats all personal information as interconnected. Tasks, notes, articles, books, movies, and TV shows exist in a single database where relationships between items are first-class citizens: 13 + 14 + - Link notes to tasks to document your work 15 + - Tag books and articles with the same taxonomy as your tasks 16 + - Track reading lists alongside project work 17 + - Connect media items to research notes 18 + 19 + This unified approach mirrors how knowledge work actually happensโ€”projects involve research, reading generates ideas, and ideas spawn tasks.
+33
website/docs/concepts/design-system.md
··· 1 + --- 2 + title: Design System 3 + sidebar_label: Design System 4 + description: Color palette guidance that keeps the terminal UI cohesive. 5 + sidebar_position: 4 6 + --- 7 + 8 + # Design System 9 + 10 + ## Color Palette and Design System 11 + 12 + Noteleaf uses a carefully chosen color palette defined in `internal/ui/palette.go`: 13 + 14 + **Brand Colors**: 15 + 16 + - **Malibu** (`#00A4FF`): Primary blue for accents and highlights 17 + - **Julep** (`#00FFB2`): Success green for completed items 18 + - **Sriracha** (`#EB4268`): Warning red for urgent/error states 19 + - **Tang** (`#FF985A`): Orange for warnings and attention 20 + - **Lichen** (`#5CDFEA`): Teal for informational elements 21 + 22 + **Neutral Palette** (Dark to Light): 23 + 24 + - **Pepper** (`#201F26`): Dark background 25 + - **BBQ** (`#2d2c35`): Secondary background 26 + - **Charcoal** (`#3A3943`): Tertiary background 27 + - **Iron** (`#4D4C57`): Borders and subtle elements 28 + - **Oyster** (`#605F6B`): Muted text 29 + - **Smoke** (`#BFBCC8`): Secondary text 30 + - **Salt** (`#F1EFEF`): Primary text in dark mode 31 + - **Butter** (`#FFFAF1`): Light background 32 + 33 + This palette ensures consistency across all UI components and provides excellent contrast for readability in terminal environments.
+111
website/docs/concepts/getting-started.md
··· 1 + --- 2 + title: Getting Started 3 + sidebar_label: Getting Started 4 + description: Installation steps, configuration overview, and ways to find help. 5 + sidebar_position: 5 6 + --- 7 + 8 + # Getting Started 9 + 10 + ## Installation and Setup 11 + 12 + ### System Requirements 13 + 14 + - Go 1.24 or higher 15 + - SQLite 3.35 or higher (usually bundled) 16 + - Terminal with 256-color support 17 + - Unix-like OS (Linux, macOS, WSL) 18 + 19 + ### Building from Source 20 + 21 + Clone the repository and build: 22 + 23 + ```sh 24 + git clone https://github.com/stormlightlabs/noteleaf 25 + cd noteleaf 26 + go build -o ./tmp/noteleaf ./cmd 27 + ``` 28 + 29 + Install to your GOPATH: 30 + 31 + ```sh 32 + go install 33 + ``` 34 + 35 + ### Database Initialization 36 + 37 + Run setup to create the database and configuration file: 38 + 39 + ```sh 40 + noteleaf setup 41 + ``` 42 + 43 + This creates: 44 + 45 + - Database at platform-specific application data directory 46 + - Configuration file at platform-specific config directory 47 + - Default settings for editor, priorities, and display options 48 + 49 + ### Seeding Sample Data 50 + 51 + For exploration and testing, populate the database with example data: 52 + 53 + ```sh 54 + noteleaf setup seed 55 + ``` 56 + 57 + This creates sample tasks, notes, books, and other items to help you understand the system's capabilities. 58 + 59 + ## Configuration Overview 60 + 61 + Configuration lives in `config.toml` at the platform-specific config directory. 62 + 63 + **Editor Settings**: 64 + 65 + ```toml 66 + [editor] 67 + command = "nvim" 68 + args = [] 69 + ``` 70 + 71 + **Task Defaults**: 72 + 73 + ```toml 74 + [task] 75 + default_priority = "medium" 76 + default_status = "pending" 77 + ``` 78 + 79 + **Display Options**: 80 + 81 + ```toml 82 + [display] 83 + date_format = "2006-01-02" 84 + time_format = "15:04" 85 + ``` 86 + 87 + View current configuration: 88 + 89 + ```sh 90 + noteleaf config show 91 + ``` 92 + 93 + Update settings: 94 + 95 + ```sh 96 + noteleaf config set editor vim 97 + ``` 98 + 99 + See the [Configuration](../Configuration.md) guide for complete options. 100 + 101 + ## Getting Help 102 + 103 + Every command includes help text: 104 + 105 + ```sh 106 + noteleaf --help 107 + noteleaf task --help 108 + noteleaf task add --help 109 + ``` 110 + 111 + For detailed command reference, run `noteleaf --help` and drill into the subcommand-specific help pages.
+26
website/docs/concepts/overview.md
··· 1 + --- 2 + title: Overview 3 + sidebar_label: Overview 4 + description: Noteleaf philosophy and reasons for a terminal-first approach. 5 + sidebar_position: 1 6 + --- 7 + 8 + # Overview 9 + 10 + ## What is Noteleaf? 11 + 12 + Noteleaf is a terminal-based personal information manager that combines task management, note-taking, media tracking, and decentralized publishing into a unified system. Built for developers and power users who prefer keyboard-driven workflows, Noteleaf brings TaskWarrior-inspired task management together with a personal knowledge base and reading list tracker. 13 + 14 + ## Why the Terminal? 15 + 16 + Terminal applications offer several advantages for focused productivity work: 17 + 18 + **Keyboard-First Interface**: Navigate and manage information without context-switching to a mouse. Every action is a keystroke away, making workflows faster once learned. 19 + 20 + **Scriptability**: Command-line tools integrate seamlessly with shell scripts, automation tools, and other CLI utilities. Noteleaf can be incorporated into your existing terminal-based workflow. 21 + 22 + **Distraction-Free**: Terminal UIs remove visual clutter and web-based distractions, keeping you focused on the task at hand. 23 + 24 + **Performance**: Native applications built with Go start instantly and consume minimal resources compared to Electron-based alternatives. 25 + 26 + **Data Ownership**: Your data lives in a local SQLite database and markdown files. No cloud dependencies, no vendor lock-in, no privacy concerns.
+92
website/docs/concepts/tui.md
··· 1 + --- 2 + title: Terminal UI 3 + sidebar_label: Terminal UI 4 + description: Navigate Noteleafโ€™s Bubble Tea interfaces and their script-friendly counterparts. 5 + sidebar_position: 6 6 + --- 7 + 8 + # Terminal UI 9 + 10 + Most list-style commands (tasks, notes, books) have two personalities: an interactive Bubble Tea view for exploration and a static text output for piping into other tools. This page explains how both modes behave. 11 + 12 + ## Interactive Mode 13 + 14 + ### Navigation 15 + 16 + - Launch the TUI with the default command (`noteleaf todo list`, `noteleaf note list`, `noteleaf media book add -i`, etc.). 17 + - Use `j`/`k` or the arrow keys to move the selection. Page Up/Down jump faster, while `g`/`G` (or Home/End) snap to the top or bottom depending on the view. 18 + - Search is always availableโ€”press `/` and start typing to filter titles, tags, projects, or notes in real time. 19 + 20 + ### Keyboard shortcuts 21 + 22 + All interactive components reuse the same key map defined in `internal/ui/data_list.go` and `internal/ui/data_table.go`: 23 + 24 + | Keys | Action | 25 + |------|--------| 26 + | `j` / `โ†“` | Move down | 27 + | `k` / `โ†‘` | Move up | 28 + | `enter` | Select the highlighted row | 29 + | `v` | Open the detail preview (when supported) | 30 + | `/` | Start search | 31 + | `r` | Refresh data from the database | 32 + | `1-9` | Jump directly to a row index | 33 + | `q`, `ctrl+c` | Quit the view | 34 + 35 + The shortcuts appear in the on-screen help so you never have to memorize them all. 36 + 37 + ### Selection and actions 38 + 39 + - Press `enter` to activate the primary action (open a note, view a task, confirm a media selection, etc.). 40 + - Some screens expose extra actions on letter keys (e.g., `a` to archive, `e` to edit). They are listed alongside the contextual help (`?`). 41 + - Interactive prompts such as `noteleaf media movie add` use the same selection model, so keyboard muscle memory carries over. 42 + 43 + ### Help screens 44 + 45 + Hit `?` at any time to open the inline help overlay. It mirrors the bindings configured for the active component and also hints at hidden actions. Press `esc`, `backspace`, or `?` again to exit. 46 + 47 + ## Static Mode 48 + 49 + ### Command-line output 50 + 51 + Add `--static` (or remove `-i`) to force plain text output. Examples: 52 + 53 + ```sh 54 + noteleaf todo list --static 55 + noteleaf note list --static --tag meeting 56 + noteleaf media book list --all --static 57 + ``` 58 + 59 + Static mode prints tables with headings so they are easy to read or parse. Commands that default to prompts (like `noteleaf media movie add`) fall back to a numbered list when you omit `-i`. 60 + 61 + ### Scripting with Noteleaf 62 + 63 + Static output is predictable, making it straightforward to combine with familiar utilities: 64 + 65 + ```sh 66 + noteleaf todo list --static --project docs | rg "pending" 67 + noteleaf note list --static | fzf 68 + ``` 69 + 70 + Because each row includes the record ID, you can feed the result back into follow-up commands (`noteleaf note view 42`, `noteleaf todo done 128`, etc.). 71 + 72 + ### Output formatting 73 + 74 + The task viewer supports the `--format` flag for quick summaries: 75 + 76 + ```sh 77 + noteleaf todo view 12 --format brief 78 + noteleaf todo view 12 --format detailed # default 79 + ``` 80 + 81 + Brief mode hides timestamps and auxiliary metadata, which keeps CI logs or chat snippets short. Future commands will inherit the same pattern. 82 + 83 + ### JSON output 84 + 85 + Use `--json` wherever it exists (currently on task views/lists) for structured output: 86 + 87 + ```sh 88 + noteleaf todo view 12 --json | jq '.status' 89 + noteleaf todo list --static --json | jq '.[] | select(.status=="pending")' 90 + ``` 91 + 92 + JSON mode ignores terminal colors and uses machine-friendly field names so you can script exports without touching the SQLite file directly.
-99
website/docs/development/ARCHITECTURE.md
··· 1 - # System Architecture 2 - 3 - Noteleaf is a CLI/TUI application for task and content management built on Go with SQLite persistence and terminal-based interfaces. 4 - 5 - ## Core Architecture 6 - 7 - ### Application Structure 8 - 9 - The application follows a layered architecture with clear separation between presentation, business logic, and data access layers. 10 - 11 - **Entry Point** - `cmd/main.go` initializes the application with dependency injection, creates handlers, and configures the Cobra command tree using the CommandGroup pattern. 12 - 13 - **CLI Framework** - Built on `spf13/cobra` with `charmbracelet/fang` providing enhanced CLI features including color schemes, versioning, and improved help formatting. 14 - 15 - **TUI Components** - Interactive interfaces use `charmbracelet/bubbletea` for state management and `charmbracelet/lipgloss` for styling with a consistent color palette system in `internal/ui/colors.go`. 16 - 17 - ### Data Layer 18 - 19 - **Database** - SQLite with schema migrations in `internal/store/sql/migrations/`. The `internal/store` package manages database connections and configuration. 20 - 21 - **Repository Pattern** - Data access abstracts through repository interfaces in `internal/repo/` with validation logic ensuring data integrity at the persistence boundary. 22 - 23 - **Models** - Entity definitions in `internal/models/` implement standardized Model interfaces with common fields (ID, Created, Modified). 24 - 25 - ### Business Logic 26 - 27 - **Handlers** - Business logic resides in `internal/handlers/` with one handler per domain (TaskHandler, NoteHandler, ArticleHandler, etc.). 28 - Handlers receive repository dependencies through constructor injection. 29 - 30 - **Services** - Domain-specific operations in `internal/services/` handle complex business workflows and external integrations. 31 - 32 - **Validation** - Schema-based validation at repository level with custom ValidationError types providing detailed field-level error messages. 33 - 34 - ## Domain Features 35 - 36 - ### Content Management 37 - 38 - **Articles** - Web scraping using `gocolly/colly` with domain-specific extraction rules stored in `internal/articles/rules/`. 39 - Articles are parsed to markdown and stored with dual file references (markdown + HTML). 40 - 41 - **Tasks** - Todo/task management inspired by TaskWarrior with filtering, status tracking, and interactive TUI views. 42 - 43 - **Notes** - Simple note management with markdown support and glamour-based terminal rendering. 44 - 45 - **Media Queues** - Separate queues for books, movies, and TV shows with status tracking and metadata management. 46 - 47 - ### User Interface 48 - 49 - **Command Groups** - Commands organized into core functionality (task, note, article, media) and management operations (setup, config, status) using the CommandGroup interface pattern. 50 - 51 - **Interactive Views** - Bubbletea-based TUI components for list navigation, item selection, and data entry with consistent styling through the lipgloss color system. 52 - 53 - **Terminal Output** - Markdown rendering through `charmbracelet/glamour` for rich text display in terminal environments. 54 - 55 - ## Dependencies 56 - 57 - **CLI/TUI** - Cobra command framework, Bubbletea state management, Lipgloss styling, Fang CLI enhancements. 58 - 59 - **Data** - SQLite driver (`mattn/go-sqlite3`), TOML configuration parsing. 60 - 61 - **Content Processing** - Colly web scraping, HTML/XML query libraries, Glamour markdown rendering, text processing utilities. 62 - 63 - **Utilities** - UUID generation, time handling, logging through `charmbracelet/log`. 64 - 65 - ## Design Decisions and Tradeoffs 66 - 67 - ### Technology Choices 68 - 69 - **Go over Rust** - Go was selected for its simplicity, excellent CLI ecosystem (Cobra, Charm libraries), and faster development velocity. 70 - While Rust + Ratatui would provide better memory safety and potentially superior performance, Go's straightforward concurrency model and mature tooling ecosystem made it the pragmatic choice for rapid prototyping and iteration. 71 - 72 - **SQLite over PostgreSQL** - SQLite provides zero-configuration deployment and sufficient performance for single-user CLI applications. 73 - The embedded database eliminates setup complexity while supporting full SQL features needed for filtering and querying. 74 - PostgreSQL would add deployment overhead without meaningful benefits for this use case. 75 - 76 - **Repository Pattern over Active Record** - Repository interfaces enable clean separation between business logic and data access, facilitating testing through dependency injection. 77 - This pattern scales better than Active Record for complex domain logic while maintaining clear boundaries between layers. 78 - 79 - ### Architectural Tradeoffs 80 - 81 - **CommandGroup Interface** - Centralizes command registration while enabling modular command organization. 82 - The pattern requires additional abstraction but provides consistent dependency injection and testing capabilities across all command groups. 83 - 84 - **Handler-based Business Logic** - Business logic in handlers rather than rich domain models keeps the codebase simple and avoids over-engineering. 85 - While this approach may not scale to complex business rules, it provides clear separation of concerns for the current feature set. 86 - 87 - **Dual Storage for Articles** - Articles store both markdown and HTML versions to balance processing speed with format flexibility. 88 - This doubles storage requirements but eliminates runtime conversion overhead and preserves original formatting. 89 - 90 - ### Component Interactions 91 - 92 - **Handler โ†’ Repository โ†’ Database** - Request flow follows a linear path from CLI commands through business logic to data persistence. 93 - This pattern ensures consistent validation and error handling while maintaining clear separation of concerns. 94 - 95 - **TUI State Management** - Bubbletea's unidirectional data flow provides predictable state updates for interactive components. 96 - The model-view-update pattern ensures consistent UI behavior across different terminal environments. 97 - 98 - **Configuration and Migration** - Application startup validates configuration and runs database migrations before initializing handlers. 99 - This fail-fast approach prevents runtime errors and ensures consistent database schema across deployments.
-292
website/docs/development/TESTING.md
··· 1 - # Testing Documentation 2 - 3 - This document outlines the testing patterns and practices used in the `noteleaf` application. 4 - 5 - ## Overview 6 - 7 - The codebase follows Go's standard testing practices with specialized testing utilities for complex scenarios. 8 - Tests use the standard library along with carefully selected dependencies like faker for data generation and BubbleTea for TUI testing. 9 - This approach keeps dependencies minimal while providing robust testing infrastructure for interactive components and complex integrations. 10 - 11 - ### Organization 12 - 13 - Each package contains its own test files alongside the source code. Test files are organized by functionality and mirror the structure of the source code they test. 14 - The codebase includes four main test utility files that provide specialized testing infrastructure: 15 - 16 - - `internal/services/test_utilities.go` - HTTP mocking and media service testing 17 - - `internal/repo/test_utilities.go` - Database testing and data generation 18 - - `internal/ui/test_utilities.go` - TUI testing framework and interactive component testing 19 - - `internal/handlers/test_utilities.go` - Handler testing with database isolation and input simulation 20 - 21 - ## Patterns 22 - 23 - ### Handler Creation 24 - 25 - Tests create real handler instances using temporary databases to ensure test isolation. 26 - Factory functions handle both database setup and handler initialization, returning both the handler and a cleanup function. 27 - 28 - ### Database Isolation 29 - 30 - Tests use temporary directories and environment variable manipulation to create isolated database instances. 31 - Each test gets its own temporary SQLite database that is automatically cleaned up after the test completes. 32 - 33 - The `setupCommandTest` function creates a temporary directory, sets `XDG_CONFIG_HOME` to point to it, and initializes the database schema. 34 - This ensures tests don't interfere with each other or with development data. 35 - 36 - ### Resource Management 37 - 38 - Tests properly manage resources using cleanup functions returned by factory methods. 39 - The cleanup function handles both handler closure and temporary directory removal. 40 - This pattern ensures complete resource cleanup even if tests fail. 41 - 42 - ### Error Handling 43 - 44 - Tests use `t.Fatal` for setup errors that prevent test execution and `t.Error` for test assertion failures. 45 - Fatal errors stop test execution while errors allow tests to continue checking other conditions. 46 - 47 - ### Context Cancellation 48 - 49 - Error case testing frequently uses context cancellation to simulate database and network failures. 50 - The pattern creates a context, immediately cancels it, then calls the function under test to verify error handling. 51 - This provides a reliable way to test error paths without requiring complex mock setups or external failure injection. 52 - 53 - ### Command Structure 54 - 55 - Command group tests verify cobra command structure including use strings, aliases, short descriptions, and subcommand presence. 56 - Tests check that commands are properly configured without executing their logic. 57 - 58 - ### Interface Compliance 59 - 60 - Tests verify interface compliance using compile-time checks with blank identifier assignments. 61 - This ensures structs implement expected interfaces without runtime overhead. 62 - 63 - ## Test Infrastructure 64 - 65 - ### Test Utility Frameworks 66 - 67 - The codebase provides comprehensive testing utilities organized by layer and functionality. 68 - Each test utility file contains specialized helpers, mocks, and test infrastructure for its respective domain. 69 - 70 - #### Database Testing Utilities 71 - 72 - `internal/repo/test_utilities.go` provides comprehensive database testing infrastructure: 73 - 74 - - **In-Memory Database Creation**: `CreateTestDB` creates isolated SQLite databases with full schema 75 - - **Sample Data Factories**: Functions like `CreateSampleTask`, `CreateSampleBook` generate realistic test data 76 - - **Faker Integration**: Uses jaswdr/faker for generating realistic fake data with `CreateFakeArticle` 77 - - **Test Setup Helpers**: `SetupTestData` creates a full set of sample data across all models 78 - - **Custom Assertions**: Generic assertion helpers like `AssertEqual`, `AssertContains`, `AssertNoError` 79 - 80 - #### HTTP Service Testing 81 - 82 - `internal/services/test_utilities.go` provides HTTP mocking and media service testing: 83 - 84 - - **Mock Configuration**: `MockConfig` structure for configuring service behavior 85 - - **Function Replacement**: `SetupMediaMocks` replaces service functions with controllable mocks 86 - - **Sample Data Access**: Helper functions that use embedded HTML samples for realistic testing 87 - - **Specialized Scenarios**: Pre-configured mock setups for success and failure scenarios 88 - - **Assertion Helpers**: Domain-specific assertions for movies, TV shows, and error conditions 89 - 90 - #### TUI Testing Framework 91 - 92 - `internal/ui/test_utilities.go` provides a comprehensive BubbleTea testing framework: 93 - 94 - - **TUITestSuite**: Complete testing infrastructure for interactive TUI components 95 - - **Controlled I/O**: `ControlledOutput` and `ControlledInput` for deterministic testing 96 - - **Message Simulation**: Key press simulation, message queuing, and timing control 97 - - **State Verification**: Model state checking and view content assertions 98 - - **Timeout Handling**: Configurable timeouts for async operations 99 - - **Mock Repository**: Test doubles for repository interfaces 100 - 101 - #### Handler Testing Infrastructure 102 - 103 - `internal/handlers/test_utilities.go` provides end-to-end handler testing: 104 - 105 - - **Environment Isolation**: `HandlerTestHelper` creates isolated test environments 106 - - **Input Simulation**: `InputSimulator` for testing interactive components that use `fmt.Scanf` 107 - - **HTTP Mocking**: Comprehensive HTTP server mocking for external API testing 108 - - **Database Helpers**: Database corruption and error scenario testing 109 - - **Editor Mocking**: `MockEditor` for testing file editing workflows 110 - - **Assertion Helpers**: Handler-specific assertions and verification functions 111 - 112 - ### Advanced Testing Patterns 113 - 114 - #### Input Simulation for Interactive Components 115 - 116 - Interactive handlers that use `fmt.Scanf` require special testing infrastructure with an `io.Reader` implementation. 117 - 118 - The `InputSimulator` provides controlled input sequences that prevent tests from hanging while maintaining coverage of interactive code paths. 119 - 120 - #### TUI Testing with BubbleTea Framework 121 - 122 - The TUI testing framework addresses the fundamental challenge of testing interactive terminal applications in a deterministic, concurrent environment. 123 - BubbleTea's message-passing architecture creates unique testing requirements that standard Go testing patterns cannot adequately address. 124 - 125 - The framework implements a controlled execution environment that replaces BubbleTea's typical program loop with a deterministic testing harness. 126 - Rather than running an actual terminal program, the "testing suite" directly manages model state transitions by simulating the Update/View cycle. 127 - This approach eliminates the non-deterministic behavior inherent in real terminal interactions while preserving the exact message flow patterns that production code experiences. 128 - 129 - State verification relies on function composition patterns where test conditions are expressed as closures that capture specific model states. 130 - The `WaitFor` mechanism uses polling with configurable timeouts, addressing the async nature of BubbleTea model updates without creating race conditions. 131 - This pattern bridges imperative test assertions with BubbleTea's declarative update model. 132 - This is inspired by front-end/TS/JS testing patterns. 133 - 134 - The framework's I/O abstraction layer replaces terminal input/output with controlled buffers that implement standard Go interfaces. 135 - This design maintains interface compatibility while providing complete control over timing and content. 136 - The controlled I/O system captures all output for later verification and injects precise input sequences, enabling complex interaction testing without external dependencies. 137 - 138 - Concurrency management uses channels and context propagation to coordinate between the testing framework and the model under test. 139 - The suite manages goroutine lifecycle and ensures proper cleanup, preventing test interference and resource leaks. 140 - This architecture supports testing of models that perform background operations or handle async events. 141 - 142 - #### HTTP Service Mocking 143 - 144 - Service testing uses HTTP mocking with request capture. A `MockServer` is instantiated, and its URL is used in test scoped services. 145 - 146 - #### Database Schema Testing 147 - 148 - Database tests use comprehensive schema setup with (automatic) cleanup 149 - 150 - #### Environment Manipulation 151 - 152 - Environment testing utilities provide controlled environment manipulation. Environment variables are restored after instantiation. 153 - 154 - ## Test Organization Patterns 155 - 156 - ### Single Root Test 157 - 158 - The preferred test organization pattern uses a single root test function with nested subtests using `t.Run`. 159 - This provides clear hierarchical organization and allows running specific test sections while maintaining shared setup and context. 160 - This pattern offers several advantages: clear test hierarchy with logical grouping, ability to run specific test sections, consistent test structure across the codebase, and shared setup that can be inherited by subtests. 161 - 162 - ### Integration vs Unit Testing 163 - 164 - The codebase emphasizes integration testing over heavy mocking by using real handlers and services to verify actual behavior rather than mocked interactions. 165 - The goal is to catch integration issues while maintaining test reliability. 166 - 167 - ### Static Output 168 - 169 - UI components support static output modes for testing. Tests capture output using bytes.Buffer and verify content using string contains checks rather than exact string matching for better test maintainability. 170 - 171 - ### Standard Output Redirection 172 - 173 - For testing functions that write to stdout, tests use a pipe redirection pattern with goroutines to capture output. 174 - The pattern saves the original stdout, redirects to a pipe, captures output in a separate goroutine, and restores stdout after the test. 175 - This ensures clean output capture without interfering with the testing framework. 176 - 177 - ## Utilities 178 - 179 - ### Test Data Generation 180 - 181 - The codebase uses sophisticated data generation strategies: 182 - 183 - - **Factory Functions**: Each package provides factory functions for creating valid test data 184 - - **Faker Integration**: Uses `jaswdr/faker` for generating realistic fake data with proper randomization 185 - - **Sample Data Creators**: Functions like `CreateSampleTask`, `CreateSampleBook` provide consistent test data 186 - - **Embedded Resources**: Services use embedded HTML samples from real API responses for realistic testing 187 - 188 - ### Assertion Helpers 189 - 190 - Custom assertion functions provide clear error messages and reduce test code duplication: 191 - 192 - - **Generic Assertions**: `AssertEqual`, `AssertNoError`, `AssertContains` for common checks 193 - - **Domain-Specific Assertions**: `AssertMovieInResults`, `AssertNoteExists` for specialized verification 194 - - **TUI Assertions**: `AssertViewContains`, `AssertModelState` for BubbleTea model testing 195 - - **HTTP Assertions**: `AssertRequestMade` for verifying HTTP interactions 196 - 197 - ### Mock Infrastructure 198 - 199 - Each layer provides specialized mocking capabilities: 200 - 201 - - **Service Mocking**: Function replacement with configurable behavior and embedded test data 202 - - **HTTP Mocking**: `HTTPMockServer` with request capture and response customization 203 - - **Input Mocking**: `InputSimulator` for deterministic interactive component testing 204 - - **Editor Mocking**: `MockEditor` for file editing workflow testing 205 - - **Repository Mocking**: `MockTaskRepository` for TUI component testing 206 - 207 - ### Environment and Resource Management 208 - 209 - Testing utilities provide comprehensive resource management: 210 - 211 - - **Environment Isolation**: `EnvironmentTestHelper` for controlled environment variable manipulation 212 - - **Database Isolation**: Temporary SQLite databases with automatic cleanup 213 - - **File System Isolation**: Temporary directories with automatic cleanup 214 - - **Process Isolation**: Handler helpers that create completely isolated test environments 215 - 216 - ## Testing CLI Commands 217 - 218 - Command group tests focus on structure verification rather than execution testing. 219 - Tests check command configuration, subcommand presence, and interface compliance. This approach ensures command trees are properly constructed without requiring complex execution mocking. 220 - 221 - ### CommandGroup Interface Testing 222 - 223 - The CommandGroup interface enables testable CLI architecture. Tests verify that command groups implement the interface correctly and return properly configured cobra commands. 224 - This pattern separates command structure from command execution. 225 - 226 - Interface compliance is tested using compile-time checks within the "Interface Implementations" subtest, ensuring all command structs properly implement the CommandGroup interface without runtime overhead. 227 - 228 - ## Performance Considerations 229 - 230 - Tests avoid expensive operations in setup functions. Handler creation uses real instances but tests focus on structure verification rather than full execution paths. 231 - This keeps test suites fast while maintaining coverage of critical functionality. 232 - 233 - The single root test pattern allows for efficient resource management where setup costs can be amortized across multiple related test cases. 234 - 235 - ## Interactive Component Testing 236 - 237 - The codebase provides comprehensive testing infrastructure for interactive components, including both terminal UI applications and command-line interfaces that require user input. 238 - 239 - ### Input Simulation Framework 240 - 241 - Interactive handlers that use `fmt.Scanf` require specialized testing infrastructure: 242 - 243 - - **InputSimulator**: Provides controlled input sequences that implement `io.Reader` 244 - - **Menu Selection Helpers**: `MenuSelection`, `MenuCancel`, `MenuSequence` for common interaction patterns 245 - - **Handler Integration**: Handlers can accept `io.Reader` for input, enabling deterministic testing 246 - - **Cleanup Management**: Automatic cleanup prevents resource leaks in test environments 247 - 248 - ### TUI Testing with BubbleTea 249 - 250 - The TUI testing framework provides complete testing infrastructure for interactive terminal interfaces: 251 - 252 - - **TUITestSuite**: Comprehensive testing framework for BubbleTea models 253 - - **Message Simulation**: Key press simulation, window resize events, and custom message handling 254 - - **State Verification**: Model state checking with custom condition functions 255 - - **View Assertions**: Content verification and output capture 256 - - **Timing Control**: Configurable timeouts and delay handling for async operations 257 - - **Mock Integration**: Repository mocking for isolated component testing 258 - 259 - ### Interactive Test Scenarios 260 - 261 - Interactive handlers should test comprehensive scenarios: 262 - 263 - - **Valid user selections** - User chooses valid menu options and inputs 264 - - **Cancellation flows** - User chooses to cancel operations (option 0 or escape keys) 265 - - **Invalid choices** - User selects out-of-range options or provides invalid input 266 - - **Navigation patterns** - Keyboard navigation, scrolling, and multi-step interactions 267 - - **Error handling** - Network errors, service failures, and data validation errors 268 - - **Empty states** - Search returns no results, empty lists, and missing data 269 - - **Edge cases** - Boundary conditions, malformed input, and resource constraints 270 - 271 - ### TUI Component Testing Patterns 272 - 273 - BubbleTea components use specialized testing patterns: 274 - 275 - - **Key Sequence Testing**: Simulate complex user interactions with timing 276 - - **State Transition Testing**: Verify model state changes through user actions 277 - - **View Content Testing**: Assert specific content appears in rendered output 278 - - **Async Operation Testing**: Handle loading states and network operations 279 - - **Responsive Design Testing**: Test different terminal sizes and window resize handling 280 - 281 - This comprehensive testing approach ensures interactive components work reliably in automated environments while maintaining full coverage of user interaction paths. 282 - 283 - ## Errors 284 - 285 - Error coverage follows a systematic approach to identify and test failure scenarios: 286 - 287 - 1. **Context Cancellation** - Primary method for testing database and network timeout scenarios 288 - 2. **Invalid Input** - Malformed data, empty inputs, boundary conditions 289 - 3. **Resource Exhaustion** - Database connection failures, memory limits 290 - 4. **Constraint Violations** - Duplicate keys, foreign key failures 291 - 5. **State Validation** - Testing functions with invalid system states 292 - 6. **Interactive Input** - Invalid user choices, cancellation handling, input simulation errors
+167
website/docs/development/building.md
··· 1 + --- 2 + title: Building Noteleaf 3 + sidebar_label: Building 4 + sidebar_position: 1 5 + description: Build configurations and development workflows. 6 + --- 7 + 8 + # Building Noteleaf 9 + 10 + Noteleaf uses [Task](https://taskfile.dev) for build automation, providing consistent workflows across development, testing, and releases. 11 + 12 + ## Prerequisites 13 + 14 + - Go 1.21 or later 15 + - [Task](https://taskfile.dev) (install via `brew install go-task/tap/go-task` on macOS) 16 + - Git (for version information) 17 + 18 + ## Build Types 19 + 20 + ### Development Build 21 + 22 + Quick build without version injection for local development: 23 + 24 + ```sh 25 + task build 26 + ``` 27 + 28 + Output: `./tmp/noteleaf` 29 + 30 + ### Development Build with Version 31 + 32 + Build with git commit hash and development tools enabled: 33 + 34 + ```sh 35 + task build:dev 36 + ``` 37 + 38 + Version format: `git describe` output (e.g., `v0.1.0-15-g1234abc`) 39 + Output: `./tmp/noteleaf` 40 + 41 + ### Release Candidate Build 42 + 43 + Build with `-rc` tag, excludes development tools: 44 + 45 + ```sh 46 + git tag v1.0.0-rc1 47 + task build:rc 48 + ``` 49 + 50 + Requirements: 51 + 52 + - Clean git tag with `-rc` suffix 53 + - Tag format: `v1.0.0-rc1`, `v2.1.0-rc2`, etc. 54 + 55 + ### Production Build 56 + 57 + Build for release with strict validation: 58 + 59 + ```sh 60 + git tag v1.0.0 61 + task build:prod 62 + ``` 63 + 64 + Requirements: 65 + 66 + - Clean semver git tag (e.g., `v1.0.0`, `v2.1.3`) 67 + - No uncommitted changes 68 + - No prerelease suffix 69 + 70 + ## Build Tags 71 + 72 + Production builds use the `prod` build tag to exclude development and seed commands: 73 + 74 + ```go 75 + //go:build !prod 76 + ``` 77 + 78 + Commands excluded from production: 79 + 80 + - `noteleaf dev` - Development utilities 81 + - `noteleaf seed` - Test data generation 82 + 83 + ## Version Information 84 + 85 + Build process injects version metadata via ldflags: 86 + 87 + ```go 88 + // internal/version/version.go 89 + var ( 90 + Version = "dev" // Git tag or "dev" 91 + Commit = "none" // Git commit hash 92 + BuildDate = "unknown" // Build timestamp 93 + ) 94 + ``` 95 + 96 + View version information: 97 + 98 + ```sh 99 + task version:show 100 + noteleaf version 101 + ``` 102 + 103 + ## Build Artifacts 104 + 105 + All binaries are built to `./tmp/` directory: 106 + 107 + ``` 108 + tmp/ 109 + โ””โ”€โ”€ noteleaf # Binary for current platform 110 + ``` 111 + 112 + ## Development Workflow 113 + 114 + Full development cycle with linting and testing: 115 + 116 + ```sh 117 + task dev 118 + ``` 119 + 120 + Runs: 121 + 122 + 1. Clean build artifacts 123 + 2. Run linters (vet, fmt) 124 + 3. Execute all tests 125 + 4. Build binary 126 + 127 + ## Manual Build 128 + 129 + Build directly with Go (bypasses Task automation): 130 + 131 + ```sh 132 + go build -o ./tmp/noteleaf ./cmd 133 + ``` 134 + 135 + With version injection: 136 + 137 + ```sh 138 + go build -ldflags "-X github.com/stormlightlabs/noteleaf/internal/version.Version=v1.0.0" -o ./tmp/noteleaf ./cmd 139 + ``` 140 + 141 + ## Cross-Platform Builds 142 + 143 + Build for specific platforms: 144 + 145 + ```sh 146 + # Linux 147 + GOOS=linux GOARCH=amd64 go build -o ./tmp/noteleaf-linux ./cmd 148 + 149 + # Windows 150 + GOOS=windows GOARCH=amd64 go build -o ./tmp/noteleaf.exe ./cmd 151 + 152 + # macOS (ARM) 153 + GOOS=darwin GOARCH=arm64 go build -o ./tmp/noteleaf-darwin-arm64 ./cmd 154 + ``` 155 + 156 + ## Clean Build 157 + 158 + Remove all build artifacts: 159 + 160 + ```sh 161 + task clean 162 + ``` 163 + 164 + Removes: 165 + 166 + - `./tmp/` directory 167 + - `coverage.out` and `coverage.html`
+331
website/docs/development/taskfile.md
··· 1 + --- 2 + title: Task Automation 3 + sidebar_label: Taskfile 4 + sidebar_position: 3 5 + description: Using Taskfile for development workflows. 6 + --- 7 + 8 + # Task Automation 9 + 10 + Noteleaf uses [Task](https://taskfile.dev) to automate common development workflows. 11 + 12 + ## Installation 13 + 14 + ### macOS 15 + 16 + ```sh 17 + brew install go-task/tap/go-task 18 + ``` 19 + 20 + ### Linux 21 + 22 + ```sh 23 + sh -c "$(curl -fsSL https://taskfile.dev/install.sh)" 24 + ``` 25 + 26 + ### Go Install 27 + 28 + ```sh 29 + go install github.com/go-task/task/v3/cmd/task@latest 30 + ``` 31 + 32 + ## Available Tasks 33 + 34 + View all tasks: 35 + 36 + ```sh 37 + task 38 + # or 39 + task --list 40 + ``` 41 + 42 + ## Common Tasks 43 + 44 + ### Build Commands 45 + 46 + **task build** - Quick development build 47 + 48 + ```sh 49 + task build 50 + ``` 51 + 52 + Output: `./tmp/noteleaf` 53 + 54 + **task build:dev** - Build with version information 55 + 56 + ```sh 57 + task build:dev 58 + ``` 59 + 60 + Includes git commit hash and build date. 61 + 62 + **task build:rc** - Release candidate build 63 + 64 + ```sh 65 + git tag v1.0.0-rc1 66 + task build:rc 67 + ``` 68 + 69 + Requires git tag with `-rc` suffix. 70 + 71 + **task build:prod** - Production build 72 + 73 + ```sh 74 + git tag v1.0.0 75 + task build:prod 76 + ``` 77 + 78 + Requires clean semver tag and no uncommitted changes. 79 + 80 + ### Testing Commands 81 + 82 + **task test** - Run all tests 83 + 84 + ```sh 85 + task test 86 + ``` 87 + 88 + **task coverage** - Generate HTML coverage report 89 + 90 + ```sh 91 + task coverage 92 + open coverage.html # View report 93 + ``` 94 + 95 + **task cov** - Terminal coverage summary 96 + 97 + ```sh 98 + task cov 99 + ``` 100 + 101 + **task check** - Lint and coverage 102 + 103 + ```sh 104 + task check 105 + ``` 106 + 107 + Runs linters and generates coverage report. 108 + 109 + ### Development Commands 110 + 111 + **task dev** - Full development workflow 112 + 113 + ```sh 114 + task dev 115 + ``` 116 + 117 + Runs: 118 + 119 + 1. `task clean` 120 + 2. `task lint` 121 + 3. `task test` 122 + 4. `task build` 123 + 124 + **task lint** - Run linters 125 + 126 + ```sh 127 + task lint 128 + ``` 129 + 130 + Runs `go vet` and `go fmt`. 131 + 132 + **task run** - Build and run 133 + 134 + ```sh 135 + task run 136 + ``` 137 + 138 + Builds then executes the binary. 139 + 140 + ### Maintenance Commands 141 + 142 + **task clean** - Remove build artifacts 143 + 144 + ```sh 145 + task clean 146 + ``` 147 + 148 + Removes: 149 + 150 + - `./tmp/` directory 151 + - Coverage files 152 + 153 + **task deps** - Download and tidy dependencies 154 + 155 + ```sh 156 + task deps 157 + ``` 158 + 159 + Runs `go mod download` and `go mod tidy`. 160 + 161 + ### Documentation Commands 162 + 163 + **task docs:generate** - Generate all documentation 164 + 165 + ```sh 166 + task docs:generate 167 + ``` 168 + 169 + Generates: 170 + 171 + - Docusaurus docs (website/docs/manual) 172 + - Man pages (docs/manual) 173 + 174 + **task docs:man** - Generate man pages 175 + 176 + ```sh 177 + task docs:man 178 + ``` 179 + 180 + **task docs:serve** - Start documentation server 181 + 182 + ```sh 183 + task docs:serve 184 + ``` 185 + 186 + Starts Docusaurus development server at <http://localhost:3000>. 187 + 188 + ### Version Commands 189 + 190 + **task version:show** - Display version info 191 + 192 + ```sh 193 + task version:show 194 + ``` 195 + 196 + Shows: 197 + 198 + - Git tag 199 + - Git commit 200 + - Git describe output 201 + - Build date 202 + 203 + **task version:validate** - Validate git tag 204 + 205 + ```sh 206 + task version:validate 207 + ``` 208 + 209 + Checks tag format for releases. 210 + 211 + ### Utility Commands 212 + 213 + **task status** - Show Go environment 214 + 215 + ```sh 216 + task status 217 + ``` 218 + 219 + Displays: 220 + 221 + - Go version 222 + - Module information 223 + - Dependencies 224 + 225 + ## Taskfile Variables 226 + 227 + Variables injected during build: 228 + 229 + ```yaml 230 + BINARY_NAME: noteleaf 231 + BUILD_DIR: ./tmp 232 + VERSION_PKG: github.com/stormlightlabs/noteleaf/internal/version 233 + GIT_COMMIT: $(git rev-parse --short HEAD) 234 + GIT_TAG: $(git describe --tags --exact-match) 235 + BUILD_DATE: $(date -u +"%Y-%m-%dT%H:%M:%SZ") 236 + ``` 237 + 238 + ## Task Dependencies 239 + 240 + Some tasks automatically trigger others: 241 + 242 + ```sh 243 + task run 244 + # Automatically runs: task build 245 + ``` 246 + 247 + ```sh 248 + task dev 249 + # Runs in sequence: 250 + # 1. task clean 251 + # 2. task lint 252 + # 3. task test 253 + # 4. task build 254 + ``` 255 + 256 + ## Custom Workflows 257 + 258 + ### Pre-commit Workflow 259 + 260 + ```sh 261 + task lint && task test 262 + ``` 263 + 264 + ### Release Preparation 265 + 266 + ```sh 267 + task check && \ 268 + git tag v1.0.0 && \ 269 + task build:prod && \ 270 + ./tmp/noteleaf version 271 + ``` 272 + 273 + ### Documentation Preview 274 + 275 + ```sh 276 + task docs:generate 277 + task docs:serve 278 + ``` 279 + 280 + ### Full CI Simulation 281 + 282 + ```sh 283 + task clean && \ 284 + task deps && \ 285 + task lint && \ 286 + task test && \ 287 + task coverage && \ 288 + task build:dev 289 + ``` 290 + 291 + ## Taskfile Structure 292 + 293 + Location: `Taskfile.yml` (project root) 294 + 295 + Key sections: 296 + 297 + - **vars**: Build variables and git information 298 + - **tasks**: Command definitions with descriptions 299 + - **deps**: Task dependencies 300 + - **preconditions**: Validation before execution 301 + 302 + ## Configuration 303 + 304 + Customize via `Taskfile.yml` or environment variables: 305 + 306 + ```yaml 307 + vars: 308 + BINARY_NAME: noteleaf 309 + BUILD_DIR: ./tmp 310 + ``` 311 + 312 + Override at runtime: 313 + 314 + ```sh 315 + BINARY_NAME=custom-noteleaf task build 316 + ``` 317 + 318 + ## Why Task Over Make? 319 + 320 + - Cross-platform (Windows, macOS, Linux) 321 + - YAML syntax (more readable than Makefile) 322 + - Built-in variable interpolation 323 + - Better dependency management 324 + - Precondition validation 325 + - Native Go integration 326 + 327 + ## Further Reading 328 + 329 + - [Task Documentation](https://taskfile.dev) 330 + - [Taskfile Schema](https://taskfile.dev/api/) 331 + - Project Taskfile: `Taskfile.yml`
+268
website/docs/development/testing.md
··· 1 + --- 2 + title: Testing 3 + sidebar_label: Testing 4 + sidebar_position: 2 5 + description: Running tests and understanding test patterns. 6 + --- 7 + 8 + # Testing 9 + 10 + Noteleaf maintains comprehensive test coverage using Go's built-in testing framework with consistent patterns across the codebase. 11 + 12 + ## Running Tests 13 + 14 + ### All Tests 15 + 16 + ```sh 17 + task test 18 + # or 19 + go test ./... 20 + ``` 21 + 22 + ### Coverage Report 23 + 24 + Generate HTML coverage report: 25 + 26 + ```sh 27 + task coverage 28 + ``` 29 + 30 + Output: `coverage.html` (opens in browser) 31 + 32 + ### Terminal Coverage 33 + 34 + View coverage in terminal: 35 + 36 + ```sh 37 + task cov 38 + ``` 39 + 40 + Shows function-level coverage percentages. 41 + 42 + ### Package-Specific Tests 43 + 44 + Test specific package: 45 + 46 + ```sh 47 + go test ./internal/repo 48 + go test ./internal/handlers 49 + go test ./cmd 50 + ``` 51 + 52 + ### Verbose Output 53 + 54 + ```sh 55 + go test -v ./... 56 + ``` 57 + 58 + ## Test Organization 59 + 60 + Tests follow a hierarchical 3-level structure: 61 + 62 + ```go 63 + func TestRepositoryName(t *testing.T) { 64 + // Setup once 65 + db := CreateTestDB(t) 66 + repos := SetupTestData(t, db) 67 + 68 + t.Run("Feature", func(t *testing.T) { 69 + t.Run("scenario description", func(t *testing.T) { 70 + // Test logic 71 + }) 72 + }) 73 + } 74 + ``` 75 + 76 + Levels: 77 + 78 + 1. Package (top function) 79 + 2. Feature (first t.Run) 80 + 3. Scenario (nested t.Run) 81 + 82 + ## Test Patterns 83 + 84 + ### Repository Tests 85 + 86 + Repository tests use scaffolding from `internal/repo/test_utilities.go`: 87 + 88 + ```go 89 + func TestTaskRepository(t *testing.T) { 90 + db := CreateTestDB(t) 91 + repos := SetupTestData(t, db) 92 + ctx := context.Background() 93 + 94 + t.Run("Create", func(t *testing.T) { 95 + t.Run("creates task successfully", func(t *testing.T) { 96 + task := NewTaskBuilder(). 97 + WithDescription("Test task"). 98 + Build() 99 + 100 + created, err := repos.Tasks.Create(ctx, task) 101 + AssertNoError(t, err, "create should succeed") 102 + AssertEqual(t, "Test task", created.Description, "description should match") 103 + }) 104 + }) 105 + } 106 + ``` 107 + 108 + ### Handler Tests 109 + 110 + Handler tests use `internal/handlers/handler_test_suite.go`: 111 + 112 + ```go 113 + func TestHandlerName(t *testing.T) { 114 + suite := NewHandlerTestSuite(t) 115 + defer suite.cleanup() 116 + handler := CreateHandler(t, NewHandlerFunc) 117 + 118 + t.Run("Feature", func(t *testing.T) { 119 + t.Run("scenario", func(t *testing.T) { 120 + AssertNoError(t, handler.Method(), "operation should succeed") 121 + }) 122 + }) 123 + } 124 + ``` 125 + 126 + ## Test Utilities 127 + 128 + ### Assertion Helpers 129 + 130 + Located in `internal/repo/test_utilities.go` and `internal/handlers/test_utilities.go`: 131 + 132 + ```go 133 + // Error checking 134 + AssertNoError(t, err, "operation should succeed") 135 + AssertError(t, err, "operation should fail") 136 + 137 + // Value comparison 138 + AssertEqual(t, expected, actual, "values should match") 139 + AssertTrue(t, condition, "should be true") 140 + AssertFalse(t, condition, "should be false") 141 + 142 + // Nil checking 143 + AssertNil(t, value, "should be nil") 144 + AssertNotNil(t, value, "should not be nil") 145 + 146 + // String operations 147 + AssertContains(t, str, substr, "should contain substring") 148 + ``` 149 + 150 + ### Test Data Builders 151 + 152 + Create test data with builders: 153 + 154 + ```go 155 + task := NewTaskBuilder(). 156 + WithDescription("Test task"). 157 + WithStatus("pending"). 158 + WithPriority("high"). 159 + WithProject("test-project"). 160 + Build() 161 + 162 + book := NewBookBuilder(). 163 + WithTitle("Test Book"). 164 + WithAuthor("Test Author"). 165 + Build() 166 + 167 + note := NewNoteBuilder(). 168 + WithTitle("Test Note"). 169 + WithContent("Test content"). 170 + Build() 171 + ``` 172 + 173 + ### Test Database 174 + 175 + In-memory SQLite for isolated tests: 176 + 177 + ```go 178 + db := CreateTestDB(t) // Automatic cleanup via t.Cleanup() 179 + ``` 180 + 181 + ### Sample Data 182 + 183 + Pre-populated test data: 184 + 185 + ```go 186 + repos := SetupTestData(t, db) 187 + // Creates tasks, notes, books, movies, TV shows 188 + ``` 189 + 190 + ## Test Naming 191 + 192 + Use direct descriptions without "should": 193 + 194 + ```go 195 + t.Run("creates task successfully", func(t *testing.T) { }) // Good 196 + t.Run("should create task", func(t *testing.T) { }) // Bad 197 + t.Run("returns error for invalid input", func(t *testing.T) { }) // Good 198 + ``` 199 + 200 + ## Test Independence 201 + 202 + Each test must be independent: 203 + 204 + - Use `CreateTestDB(t)` for isolated database 205 + - Don't rely on test execution order 206 + - Clean up resources with `t.Cleanup()` 207 + - Avoid package-level state 208 + 209 + ## Coverage Targets 210 + 211 + Maintain high coverage for: 212 + 213 + - Repository layer (data access) 214 + - Handler layer (business logic) 215 + - Services (external integrations) 216 + - Models (data validation) 217 + 218 + Current coverage visible via: 219 + 220 + ```sh 221 + task cov 222 + ``` 223 + 224 + ## Continuous Integration 225 + 226 + Tests run automatically on: 227 + 228 + - Pull requests 229 + - Main branch commits 230 + - Release builds 231 + 232 + CI configuration validates: 233 + 234 + - All tests pass 235 + - No race conditions 236 + - Coverage thresholds met 237 + 238 + ## Debugging Tests 239 + 240 + ### Run Single Test 241 + 242 + ```sh 243 + go test -run TestTaskRepository ./internal/repo 244 + go test -run TestTaskRepository/Create ./internal/repo 245 + ``` 246 + 247 + ### Race Detector 248 + 249 + ```sh 250 + go test -race ./... 251 + ``` 252 + 253 + ### Verbose with Stack Traces 254 + 255 + ```sh 256 + go test -v -race ./internal/repo 2>&1 | grep -A 10 "FAIL" 257 + ``` 258 + 259 + ## Best Practices 260 + 261 + 1. Write tests for all public APIs 262 + 2. Use builders for complex test data 263 + 3. Apply semantic assertion helpers 264 + 4. Keep tests focused and readable 265 + 5. Test both success and error paths 266 + 6. Avoid brittle time-based tests 267 + 7. Mock external dependencies 268 + 8. Use table-driven tests for variations
-340
website/docs/examples/articles.md
··· 1 - # Article Examples 2 - 3 - Examples of saving and managing articles using Noteleaf. 4 - 5 - ## Saving Articles 6 - 7 - ### Save Article from URL 8 - 9 - ```sh 10 - noteleaf article add https://example.com/interesting-article 11 - ``` 12 - 13 - ### Save with Custom Title 14 - 15 - ```sh 16 - noteleaf article add https://example.com/article --title "My Custom Title" 17 - ``` 18 - 19 - ### Save Multiple Articles 20 - 21 - ```sh 22 - noteleaf article add https://example.com/article-1 23 - noteleaf article add https://example.com/article-2 24 - noteleaf article add https://example.com/article-3 25 - ``` 26 - 27 - ## Viewing Articles 28 - 29 - ### List All Articles 30 - 31 - ```sh 32 - noteleaf article list 33 - ``` 34 - 35 - ### Filter by Author 36 - 37 - ```sh 38 - noteleaf article list --author "Jane Smith" 39 - noteleaf article list --author "John Doe" 40 - ``` 41 - 42 - ### Filter with Query 43 - 44 - ```sh 45 - noteleaf article list --query "golang" 46 - noteleaf article list --query "machine learning" 47 - ``` 48 - 49 - ### Limit Results 50 - 51 - ```sh 52 - noteleaf article list --limit 10 53 - noteleaf article list --limit 5 54 - ``` 55 - 56 - ### View Article Content 57 - 58 - Display in terminal: 59 - 60 - ```sh 61 - noteleaf article view 1 62 - ``` 63 - 64 - Read in browser: 65 - 66 - ```sh 67 - noteleaf article read 1 68 - ``` 69 - 70 - ## Managing Articles 71 - 72 - ### Update Article Metadata 73 - 74 - Update title: 75 - 76 - ```sh 77 - noteleaf article update 1 --title "Updated Title" 78 - ``` 79 - 80 - Update author: 81 - 82 - ```sh 83 - noteleaf article update 1 --author "Jane Doe" 84 - ``` 85 - 86 - Add notes: 87 - 88 - ```sh 89 - noteleaf article update 1 --notes "Great insights on API design" 90 - ``` 91 - 92 - ### Remove Article 93 - 94 - ```sh 95 - noteleaf article remove 1 96 - ``` 97 - 98 - Remove multiple: 99 - 100 - ```sh 101 - noteleaf article remove 1 2 3 102 - ``` 103 - 104 - ## Common Workflows 105 - 106 - ### Reading List Management 107 - 108 - Save articles to read later: 109 - 110 - ```sh 111 - noteleaf article add https://blog.example.com/microservices 112 - noteleaf article add https://dev.to/understanding-async 113 - noteleaf article add https://medium.com/best-practices 114 - 115 - # View reading list 116 - noteleaf article list 117 - ``` 118 - 119 - ### Research Collection 120 - 121 - Collect articles for research: 122 - 123 - ```sh 124 - # Save research articles 125 - noteleaf article add https://arxiv.org/paper1 --notes "Research: ML optimization" 126 - noteleaf article add https://arxiv.org/paper2 --notes "Research: Neural networks" 127 - 128 - # Find research articles 129 - noteleaf article list --query "Research" 130 - ``` 131 - 132 - ### Technical Documentation 133 - 134 - Archive technical articles: 135 - 136 - ```sh 137 - noteleaf article add https://docs.example.com/api-guide --notes "Category: Documentation" 138 - noteleaf article add https://tutorials.example.com/setup --notes "Category: Tutorial" 139 - 140 - # Find documentation 141 - noteleaf article list --query "Documentation" 142 - ``` 143 - 144 - ### Author Following 145 - 146 - Track articles by favorite authors: 147 - 148 - ```sh 149 - # Save articles 150 - noteleaf article add https://blog.author1.com/post1 --author "Martin Fowler" 151 - noteleaf article add https://blog.author1.com/post2 --author "Martin Fowler" 152 - 153 - # View articles by author 154 - noteleaf article list --author "Martin Fowler" 155 - ``` 156 - 157 - ### Topic Collections 158 - 159 - Organize by topic using notes: 160 - 161 - ```sh 162 - # Backend articles 163 - noteleaf article add https://example.com/databases --notes "Topic: Backend, Database" 164 - noteleaf article add https://example.com/caching --notes "Topic: Backend, Performance" 165 - 166 - # Frontend articles 167 - noteleaf article add https://example.com/react --notes "Topic: Frontend, React" 168 - noteleaf article add https://example.com/css --notes "Topic: Frontend, CSS" 169 - 170 - # Find by topic 171 - noteleaf article list --query "Backend" 172 - noteleaf article list --query "Frontend" 173 - ``` 174 - 175 - ### Daily Reading Routine 176 - 177 - Save articles during the day: 178 - 179 - ```sh 180 - # Morning 181 - noteleaf article add https://news.ycombinator.com/article1 182 - noteleaf article add https://reddit.com/r/programming/article2 183 - 184 - # Evening - review saved articles 185 - noteleaf article list 186 - noteleaf article view 1 187 - noteleaf article view 2 188 - ``` 189 - 190 - ### Offline Reading 191 - 192 - Save articles for offline access: 193 - 194 - ```sh 195 - # Save articles before travel 196 - noteleaf article add https://longform.com/essay1 197 - noteleaf article add https://magazine.com/feature 198 - 199 - # Read offline (articles are saved locally) 200 - noteleaf article view 1 201 - noteleaf article view 2 202 - ``` 203 - 204 - ### Archive and Cleanup 205 - 206 - Remove read articles: 207 - 208 - ```sh 209 - # List articles 210 - noteleaf article list 211 - 212 - # Remove articles you've read 213 - noteleaf article remove 1 2 3 4 5 214 - 215 - # Keep only recent articles (manual filtering) 216 - noteleaf article list --limit 20 217 - ``` 218 - 219 - ### Share-worthy Content 220 - 221 - Mark articles worth sharing: 222 - 223 - ```sh 224 - noteleaf article add https://excellent.article.com --notes "Share: Twitter, Newsletter" 225 - noteleaf article add https://must-read.com/post --notes "Share: Team, Blog" 226 - 227 - # Find share-worthy articles 228 - noteleaf article list --query "Share" 229 - ``` 230 - 231 - ### Learning Path 232 - 233 - Create structured learning collections: 234 - 235 - ```sh 236 - # Beginner articles 237 - noteleaf article add https://tutorial.com/intro --notes "Level: Beginner, Go" 238 - noteleaf article add https://tutorial.com/basics --notes "Level: Beginner, Go" 239 - 240 - # Advanced articles 241 - noteleaf article add https://advanced.com/patterns --notes "Level: Advanced, Go" 242 - 243 - # Find by level 244 - noteleaf article list --query "Beginner" 245 - noteleaf article list --query "Advanced" 246 - ``` 247 - 248 - ### Weekly Digests 249 - 250 - Collect interesting articles weekly: 251 - 252 - ```sh 253 - # Week 1 254 - noteleaf article add https://example.com/week1-1 --notes "Week: 2024-W01" 255 - noteleaf article add https://example.com/week1-2 --notes "Week: 2024-W01" 256 - 257 - # Week 2 258 - noteleaf article add https://example.com/week2-1 --notes "Week: 2024-W02" 259 - 260 - # Review week's articles 261 - noteleaf article list --query "2024-W01" 262 - ``` 263 - 264 - ## Exporting Articles 265 - 266 - ### Export Article to File 267 - 268 - ```sh 269 - noteleaf article export 1 --format markdown > article.md 270 - noteleaf article export 1 --format html > article.html 271 - ``` 272 - 273 - ### Export Multiple Articles 274 - 275 - ```sh 276 - noteleaf article export --all --format markdown --output articles/ 277 - ``` 278 - 279 - ### Export by Query 280 - 281 - ```sh 282 - noteleaf article export --query "golang" --format markdown --output go-articles/ 283 - ``` 284 - 285 - ## Integration with Notes 286 - 287 - ### Create Note from Article 288 - 289 - After reading: 290 - 291 - ```sh 292 - # Read article 293 - noteleaf article view 1 294 - 295 - # Create summary note 296 - noteleaf note create "Article Summary: Title" " 297 - Source: [Article URL] 298 - Author: [Author Name] 299 - 300 - Key Points: 301 - - Point 1 302 - - Point 2 303 - 304 - My Thoughts: 305 - - Observation 1 306 - - Observation 2 307 - " --tags article-summary,topic 308 - ``` 309 - 310 - ### Link Article to Task 311 - 312 - Create follow-up task: 313 - 314 - ```sh 315 - # Save article 316 - noteleaf article add https://example.com/implement-feature 317 - 318 - # Create related task 319 - noteleaf task add "Implement feature from article #1" --tags implementation 320 - ``` 321 - 322 - ## Article Statistics 323 - 324 - ### Count Articles 325 - 326 - ```sh 327 - noteleaf article list | wc -l 328 - ``` 329 - 330 - ### Articles by Author 331 - 332 - ```sh 333 - noteleaf article list --author "Author Name" | wc -l 334 - ``` 335 - 336 - ### Articles by Topic 337 - 338 - ```sh 339 - noteleaf article list --query "topic" | wc -l 340 - ```
-397
website/docs/examples/media.md
··· 1 - # Media Examples 2 - 3 - Examples of managing your reading lists and watch queues using Noteleaf. 4 - 5 - ## Books 6 - 7 - ### Adding Books 8 - 9 - Search and add from Open Library: 10 - 11 - ```sh 12 - noteleaf media book add "The Name of the Wind" 13 - noteleaf media book add "Project Hail Mary" 14 - noteleaf media book add "Dune" 15 - ``` 16 - 17 - Add by author: 18 - 19 - ```sh 20 - noteleaf media book add "Foundation by Isaac Asimov" 21 - ``` 22 - 23 - Add with specific year: 24 - 25 - ```sh 26 - noteleaf media book add "1984 by George Orwell 1949" 27 - ``` 28 - 29 - ### Viewing Books 30 - 31 - List all books: 32 - 33 - ```sh 34 - noteleaf media book list 35 - ``` 36 - 37 - Filter by status: 38 - 39 - ```sh 40 - noteleaf media book list --status queued 41 - noteleaf media book list --status reading 42 - noteleaf media book list --status finished 43 - ``` 44 - 45 - ### Managing Reading Status 46 - 47 - Start reading: 48 - 49 - ```sh 50 - noteleaf media book reading 1 51 - ``` 52 - 53 - Mark as finished: 54 - 55 - ```sh 56 - noteleaf media book finished 1 57 - ``` 58 - 59 - ### Tracking Progress 60 - 61 - Update reading progress (percentage): 62 - 63 - ```sh 64 - noteleaf media book progress 1 25 65 - noteleaf media book progress 1 50 66 - noteleaf media book progress 1 75 67 - ``` 68 - 69 - Update with page numbers: 70 - 71 - ```sh 72 - noteleaf media book progress 1 150 --total 400 73 - ``` 74 - 75 - ### Book Details 76 - 77 - View book details: 78 - 79 - ```sh 80 - noteleaf media book view 1 81 - ``` 82 - 83 - ### Updating Book Information 84 - 85 - Update book notes: 86 - 87 - ```sh 88 - noteleaf media book update 1 --notes "Excellent worldbuilding and magic system" 89 - ``` 90 - 91 - Add rating: 92 - 93 - ```sh 94 - noteleaf media book update 1 --rating 5 95 - ``` 96 - 97 - ### Removing Books 98 - 99 - Remove from list: 100 - 101 - ```sh 102 - noteleaf media book remove 1 103 - ``` 104 - 105 - ## Movies 106 - 107 - ### Adding Movies 108 - 109 - Add movie: 110 - 111 - ```sh 112 - noteleaf media movie add "The Matrix" 113 - noteleaf media movie add "Inception" 114 - noteleaf media movie add "Interstellar" 115 - ``` 116 - 117 - Add with year: 118 - 119 - ```sh 120 - noteleaf media movie add "Blade Runner 1982" 121 - ``` 122 - 123 - ### Viewing Movies 124 - 125 - List all movies: 126 - 127 - ```sh 128 - noteleaf media movie list 129 - ``` 130 - 131 - Filter by status: 132 - 133 - ```sh 134 - noteleaf media movie list --status queued 135 - noteleaf media movie list --status watched 136 - ``` 137 - 138 - ### Managing Watch Status 139 - 140 - Mark as watched: 141 - 142 - ```sh 143 - noteleaf media movie watched 1 144 - ``` 145 - 146 - ### Movie Details 147 - 148 - View movie details: 149 - 150 - ```sh 151 - noteleaf media movie view 1 152 - ``` 153 - 154 - ### Updating Movie Information 155 - 156 - Add notes and rating: 157 - 158 - ```sh 159 - noteleaf media movie update 1 --notes "Mind-bending sci-fi" --rating 5 160 - ``` 161 - 162 - ### Removing Movies 163 - 164 - Remove from list: 165 - 166 - ```sh 167 - noteleaf media movie remove 1 168 - ``` 169 - 170 - ## TV Shows 171 - 172 - ### Adding TV Shows 173 - 174 - Add TV show: 175 - 176 - ```sh 177 - noteleaf media tv add "Breaking Bad" 178 - noteleaf media tv add "The Wire" 179 - noteleaf media tv add "Better Call Saul" 180 - ``` 181 - 182 - ### Viewing TV Shows 183 - 184 - List all shows: 185 - 186 - ```sh 187 - noteleaf media tv list 188 - ``` 189 - 190 - Filter by status: 191 - 192 - ```sh 193 - noteleaf media tv list --status queued 194 - noteleaf media tv list --status watching 195 - noteleaf media tv list --status watched 196 - ``` 197 - 198 - ### Managing Watch Status 199 - 200 - Start watching: 201 - 202 - ```sh 203 - noteleaf media tv watching 1 204 - ``` 205 - 206 - Mark as finished: 207 - 208 - ```sh 209 - noteleaf media tv watched 1 210 - ``` 211 - 212 - Put on hold: 213 - 214 - ```sh 215 - noteleaf media tv update 1 --status on-hold 216 - ``` 217 - 218 - ### TV Show Details 219 - 220 - View show details: 221 - 222 - ```sh 223 - noteleaf media tv view 1 224 - ``` 225 - 226 - ### Updating TV Show Information 227 - 228 - Update current episode: 229 - 230 - ```sh 231 - noteleaf media tv update 1 --season 2 --episode 5 232 - ``` 233 - 234 - Add notes and rating: 235 - 236 - ```sh 237 - noteleaf media tv update 1 --notes "Intense character development" --rating 5 238 - ``` 239 - 240 - ### Removing TV Shows 241 - 242 - Remove from list: 243 - 244 - ```sh 245 - noteleaf media tv remove 1 246 - ``` 247 - 248 - ## Common Workflows 249 - 250 - ### Weekend Watch List 251 - 252 - Plan your weekend viewing: 253 - 254 - ```sh 255 - # Add movies 256 - noteleaf media movie add "The Shawshank Redemption" 257 - noteleaf media movie add "Pulp Fiction" 258 - noteleaf media movie add "Forrest Gump" 259 - 260 - # View queue 261 - noteleaf media movie list --status queued 262 - ``` 263 - 264 - ### Reading Challenge 265 - 266 - Track annual reading goal: 267 - 268 - ```sh 269 - # Add books to queue 270 - noteleaf media book add "The Lord of the Rings" 271 - noteleaf media book add "The Hobbit" 272 - noteleaf media book add "Mistborn" 273 - 274 - # Check progress 275 - noteleaf media book list --status finished 276 - noteleaf media book list --status reading 277 - ``` 278 - 279 - ### Binge Watching Tracker 280 - 281 - Track TV series progress: 282 - 283 - ```sh 284 - # Start series 285 - noteleaf media tv add "Game of Thrones" 286 - noteleaf media tv watching 1 287 - 288 - # Update progress 289 - noteleaf media tv update 1 --season 1 --episode 1 290 - noteleaf media tv update 1 --season 1 --episode 2 291 - 292 - # View current shows 293 - noteleaf media tv list --status watching 294 - ``` 295 - 296 - ### Media Recommendations 297 - 298 - Keep track of recommendations: 299 - 300 - ```sh 301 - # Add recommended items 302 - noteleaf media book add "Sapiens" --notes "Recommended by John" 303 - noteleaf media movie add "Parasite" --notes "Won Best Picture 2020" 304 - noteleaf media tv add "Succession" --notes "From Reddit recommendations" 305 - 306 - # View recommendations 307 - noteleaf media book list --static | grep "Recommended" 308 - ``` 309 - 310 - ### Review and Rating 311 - 312 - After finishing, add review: 313 - 314 - ```sh 315 - # Book review 316 - noteleaf media book finished 1 317 - noteleaf media book update 1 \ 318 - --rating 5 \ 319 - --notes "Masterful storytelling. The magic system is one of the best in fantasy." 320 - 321 - # Movie review 322 - noteleaf media movie watched 2 323 - noteleaf media movie update 2 \ 324 - --rating 4 \ 325 - --notes "Great cinematography but slow pacing in second act." 326 - 327 - # TV show review 328 - noteleaf media tv watched 3 329 - noteleaf media tv update 3 \ 330 - --rating 5 \ 331 - --notes "Best character development I've seen. Final season was perfect." 332 - ``` 333 - 334 - ### Genre Organization 335 - 336 - Organize by genre using notes: 337 - 338 - ```sh 339 - noteleaf media book add "Snow Crash" --notes "Genre: Cyberpunk" 340 - noteleaf media book add "Neuromancer" --notes "Genre: Cyberpunk" 341 - noteleaf media book add "The Expanse" --notes "Genre: Space Opera" 342 - 343 - # Find by genre 344 - noteleaf media book list --static | grep "Cyberpunk" 345 - ``` 346 - 347 - ### Currently Consuming 348 - 349 - See what you're currently reading/watching: 350 - 351 - ```sh 352 - noteleaf media book list --status reading 353 - noteleaf media tv list --status watching 354 - ``` 355 - 356 - ### Completed This Month 357 - 358 - View completed items: 359 - 360 - ```sh 361 - noteleaf media book list --status finished 362 - noteleaf media movie list --status watched 363 - noteleaf media tv list --status watched 364 - ``` 365 - 366 - ### Clear Finished Items 367 - 368 - Archive or remove completed media: 369 - 370 - ```sh 371 - # Remove watched movies 372 - noteleaf media movie remove 1 2 3 373 - 374 - # Remove finished books 375 - noteleaf media book remove 4 5 6 376 - ``` 377 - 378 - ## Statistics and Reports 379 - 380 - ### Reading Statistics 381 - 382 - Count books by status: 383 - 384 - ```sh 385 - echo "Queued: $(noteleaf media book list --status queued --static | wc -l)" 386 - echo "Reading: $(noteleaf media book list --status reading --static | wc -l)" 387 - echo "Finished: $(noteleaf media book list --status finished --static | wc -l)" 388 - ``` 389 - 390 - ### Viewing Habits 391 - 392 - Track watch queue size: 393 - 394 - ```sh 395 - echo "Movies to watch: $(noteleaf media movie list --status queued --static | wc -l)" 396 - echo "Shows in progress: $(noteleaf media tv list --status watching --static | wc -l)" 397 - ```
-294
website/docs/examples/notes.md
··· 1 - # Note Examples 2 - 3 - Examples of note-taking workflows using Noteleaf. 4 - 5 - ## Creating Notes 6 - 7 - ### Create Note from Command Line 8 - 9 - ```sh 10 - noteleaf note create "Meeting Notes" "Discussed Q4 roadmap and priorities" 11 - ``` 12 - 13 - ### Create Note with Tags 14 - 15 - ```sh 16 - noteleaf note create "API Design Ideas" "REST vs GraphQL considerations" --tags api,design 17 - ``` 18 - 19 - ### Create Note from File 20 - 21 - ```sh 22 - noteleaf note create --file notes.md 23 - ``` 24 - 25 - ### Create Note Interactively 26 - 27 - Opens your editor for composition: 28 - 29 - ```sh 30 - noteleaf note create --interactive 31 - ``` 32 - 33 - Specify editor: 34 - 35 - ```sh 36 - EDITOR=vim noteleaf note create --interactive 37 - ``` 38 - 39 - ### Create Note with Multiple Paragraphs 40 - 41 - ```sh 42 - noteleaf note create "Project Retrospective" " 43 - What went well: 44 - - Good team collaboration 45 - - Met all deadlines 46 - - Quality code reviews 47 - 48 - What to improve: 49 - - Better documentation 50 - - More automated tests 51 - - Earlier stakeholder feedback 52 - " 53 - ``` 54 - 55 - ## Viewing Notes 56 - 57 - ### List All Notes 58 - 59 - Interactive mode: 60 - 61 - ```sh 62 - noteleaf note list 63 - ``` 64 - 65 - Static output: 66 - 67 - ```sh 68 - noteleaf note list --static 69 - ``` 70 - 71 - ### Filter by Tags 72 - 73 - ```sh 74 - noteleaf note list --tags meeting 75 - noteleaf note list --tags api,design 76 - ``` 77 - 78 - ### View Archived Notes 79 - 80 - ```sh 81 - noteleaf note list --archived 82 - ``` 83 - 84 - ### Read a Note 85 - 86 - Display note content: 87 - 88 - ```sh 89 - noteleaf note read 1 90 - ``` 91 - 92 - ### Search Notes 93 - 94 - ```sh 95 - noteleaf note search "API design" 96 - noteleaf note search "meeting notes" 97 - ``` 98 - 99 - ## Editing Notes 100 - 101 - ### Edit Note in Editor 102 - 103 - Opens note in your editor: 104 - 105 - ```sh 106 - noteleaf note edit 1 107 - ``` 108 - 109 - With specific editor: 110 - 111 - ```sh 112 - EDITOR=nvim noteleaf note edit 1 113 - ``` 114 - 115 - ### Update Note Title 116 - 117 - ```sh 118 - noteleaf note update 1 --title "Updated Meeting Notes" 119 - ``` 120 - 121 - ### Add Tags to Note 122 - 123 - ```sh 124 - noteleaf note tag 1 --add important,todo 125 - ``` 126 - 127 - ### Remove Tags from Note 128 - 129 - ```sh 130 - noteleaf note tag 1 --remove draft 131 - ``` 132 - 133 - ## Organizing Notes 134 - 135 - ### Archive a Note 136 - 137 - ```sh 138 - noteleaf note archive 1 139 - ``` 140 - 141 - ### Unarchive a Note 142 - 143 - ```sh 144 - noteleaf note unarchive 1 145 - ``` 146 - 147 - ### Delete a Note 148 - 149 - ```sh 150 - noteleaf note remove 1 151 - ``` 152 - 153 - With confirmation: 154 - 155 - ```sh 156 - noteleaf note remove 1 --confirm 157 - ``` 158 - 159 - ## Common Workflows 160 - 161 - ### Quick Meeting Notes 162 - 163 - ```sh 164 - noteleaf note create "Team Standup $(date +%Y-%m-%d)" --interactive --tags meeting,standup 165 - ``` 166 - 167 - ### Project Documentation 168 - 169 - ```sh 170 - noteleaf note create "Project Architecture" "$(cat architecture.md)" --tags docs,architecture 171 - ``` 172 - 173 - ### Research Notes 174 - 175 - Create research note: 176 - 177 - ```sh 178 - noteleaf note create "GraphQL Research" --interactive --tags research,api 179 - ``` 180 - 181 - List all research notes: 182 - 183 - ```sh 184 - noteleaf note list --tags research 185 - ``` 186 - 187 - ### Code Snippets 188 - 189 - ```sh 190 - noteleaf note create "Useful Git Commands" " 191 - # Rebase last 3 commits 192 - git rebase -i HEAD~3 193 - 194 - # Undo last commit 195 - git reset --soft HEAD~1 196 - 197 - # Show files changed in commit 198 - git show --name-only <commit> 199 - " --tags git,snippets,reference 200 - ``` 201 - 202 - ### Daily Journal 203 - 204 - ```sh 205 - noteleaf note create "Journal $(date +%Y-%m-%d)" --interactive --tags journal 206 - ``` 207 - 208 - ### Ideas and Brainstorming 209 - 210 - ```sh 211 - noteleaf note create "Product Ideas" --interactive --tags ideas,product 212 - ``` 213 - 214 - List all ideas: 215 - 216 - ```sh 217 - noteleaf note list --tags ideas 218 - ``` 219 - 220 - ## Exporting Notes 221 - 222 - ### Export Single Note 223 - 224 - ```sh 225 - noteleaf note export 1 --format markdown > note.md 226 - noteleaf note export 1 --format html > note.html 227 - ``` 228 - 229 - ### Export All Notes 230 - 231 - ```sh 232 - noteleaf note export --all --format markdown --output notes/ 233 - ``` 234 - 235 - ### Export Notes by Tag 236 - 237 - ```sh 238 - noteleaf note export --tags meeting --format markdown --output meetings/ 239 - ``` 240 - 241 - ## Advanced Usage 242 - 243 - ### Template-based Notes 244 - 245 - Create a note template file: 246 - 247 - ```sh 248 - cat > ~/templates/meeting.md << 'EOF' 249 - # Meeting: [TITLE] 250 - Date: [DATE] 251 - Attendees: [NAMES] 252 - 253 - ## Agenda 254 - - 255 - 256 - ## Discussion 257 - - 258 - 259 - ## Action Items 260 - - [ ] 261 - 262 - ## Next Meeting 263 - Date: 264 - EOF 265 - ``` 266 - 267 - Use template: 268 - 269 - ```sh 270 - noteleaf note create --file ~/templates/meeting.md 271 - ``` 272 - 273 - ### Linking Notes 274 - 275 - Reference other notes in content: 276 - 277 - ```sh 278 - noteleaf note create "Implementation Plan" " 279 - Based on the design in Note #5, we will: 280 - 1. Set up database schema (see Note #12) 281 - 2. Implement API endpoints 282 - 3. Add frontend components 283 - 284 - Related: Note #5 (Design), Note #12 (Schema) 285 - " --tags implementation,plan 286 - ``` 287 - 288 - ### Note Statistics 289 - 290 - View note count by tag: 291 - 292 - ```sh 293 - noteleaf note list --static | grep -c "tag:meeting" 294 - ```
-482
website/docs/examples/publication.md
··· 1 - # Publication Examples 2 - 3 - Examples of publishing notes to leaflet.pub using the AT Protocol integration. 4 - 5 - ## Overview 6 - 7 - The publication system allows you to sync your local notes with leaflet.pub, an AT Protocol-based publishing platform. You can pull drafts from leaflet, publish local notes, and maintain a synchronized writing workflow across platforms. 8 - 9 - ## Authentication 10 - 11 - ### Initial Authentication 12 - 13 - Authenticate with your BlueSky account: 14 - 15 - ```sh 16 - noteleaf pub auth username.bsky.social 17 - ``` 18 - 19 - This will prompt for your app password interactively. 20 - 21 - ### Authenticate with Password Flag 22 - 23 - Provide credentials directly: 24 - 25 - ```sh 26 - noteleaf pub auth username.bsky.social --password "your-app-password" 27 - ``` 28 - 29 - ### Creating an App Password 30 - 31 - 1. Visit [bsky.app/settings/app-passwords](https://bsky.app/settings/app-passwords) 32 - 2. Create a new app password named "noteleaf" 33 - 3. Use that password (not your main password) for authentication 34 - 35 - ### Check Authentication Status 36 - 37 - ```sh 38 - noteleaf pub status 39 - ``` 40 - 41 - ## Pulling Documents from Leaflet 42 - 43 - ### Pull All Documents 44 - 45 - Fetch all drafts and published documents: 46 - 47 - ```sh 48 - noteleaf pub pull 49 - ``` 50 - 51 - This will: 52 - - Connect to your leaflet account 53 - - Fetch all documents in your repository 54 - - Create new notes for documents not yet synced 55 - - Update existing notes that have changed 56 - 57 - ### After Pulling 58 - 59 - List the synced notes: 60 - 61 - ```sh 62 - noteleaf pub list 63 - ``` 64 - 65 - View synced notes interactively: 66 - 67 - ```sh 68 - noteleaf pub list --interactive 69 - ``` 70 - 71 - ## Publishing Local Notes 72 - 73 - ### Publish a Note 74 - 75 - Create a new document on leaflet from a local note: 76 - 77 - ```sh 78 - noteleaf pub post 123 79 - ``` 80 - 81 - ### Publish as Draft 82 - 83 - Create as draft instead of publishing immediately: 84 - 85 - ```sh 86 - noteleaf pub post 123 --draft 87 - ``` 88 - 89 - ### Preview Before Publishing 90 - 91 - See what would be posted without actually posting: 92 - 93 - ```sh 94 - noteleaf pub post 123 --preview 95 - ``` 96 - 97 - ### Validate Conversion 98 - 99 - Check if markdown conversion will work: 100 - 101 - ```sh 102 - noteleaf pub post 123 --validate 103 - ``` 104 - 105 - ## Updating Published Documents 106 - 107 - ### Update an Existing Document 108 - 109 - Update a previously published note: 110 - 111 - ```sh 112 - noteleaf pub patch 123 113 - ``` 114 - 115 - ### Preview Update 116 - 117 - See what would be updated: 118 - 119 - ```sh 120 - noteleaf pub patch 123 --preview 121 - ``` 122 - 123 - ### Validate Update 124 - 125 - Check conversion before updating: 126 - 127 - ```sh 128 - noteleaf pub patch 123 --validate 129 - ``` 130 - 131 - ## Batch Operations 132 - 133 - ### Publish Multiple Notes 134 - 135 - Create or update multiple documents at once: 136 - 137 - ```sh 138 - noteleaf pub push 1 2 3 4 5 139 - ``` 140 - 141 - This will: 142 - - Create new documents for notes never published 143 - - Update existing documents for notes already on leaflet 144 - 145 - ### Batch Publish as Drafts 146 - 147 - ```sh 148 - noteleaf pub push 10 11 12 --draft 149 - ``` 150 - 151 - ## Viewing Publications 152 - 153 - ### List All Synced Notes 154 - 155 - ```sh 156 - noteleaf pub list 157 - ``` 158 - 159 - Aliases: 160 - ```sh 161 - noteleaf pub ls 162 - ``` 163 - 164 - ### Filter by Status 165 - 166 - Published documents only: 167 - ```sh 168 - noteleaf pub list --published 169 - ``` 170 - 171 - Drafts only: 172 - ```sh 173 - noteleaf pub list --draft 174 - ``` 175 - 176 - All documents: 177 - ```sh 178 - noteleaf pub list --all 179 - ``` 180 - 181 - ### Interactive Browser 182 - 183 - Browse with TUI interface: 184 - 185 - ```sh 186 - noteleaf pub list --interactive 187 - noteleaf pub list -i 188 - ``` 189 - 190 - With filters: 191 - ```sh 192 - noteleaf pub list --published --interactive 193 - noteleaf pub list --draft -i 194 - ``` 195 - 196 - ## Common Workflows 197 - 198 - ### Initial Setup and Pull 199 - 200 - Set up leaflet integration and pull existing documents: 201 - 202 - ```sh 203 - # Authenticate 204 - noteleaf pub auth username.bsky.social 205 - 206 - # Check status 207 - noteleaf pub status 208 - 209 - # Pull all documents 210 - noteleaf pub pull 211 - 212 - # View synced notes 213 - noteleaf pub list --interactive 214 - ``` 215 - 216 - ### Publishing Workflow 217 - 218 - Write locally, then publish to leaflet: 219 - 220 - ```sh 221 - # Create a note 222 - noteleaf note create "My Blog Post" --interactive 223 - 224 - # List notes to get ID 225 - noteleaf note list 226 - 227 - # Publish as draft first 228 - noteleaf pub post 42 --draft 229 - 230 - # Review draft on leaflet.pub 231 - # Make edits locally 232 - noteleaf note edit 42 233 - 234 - # Update the draft 235 - noteleaf pub patch 42 236 - 237 - # When ready, republish without --draft flag 238 - noteleaf pub post 42 239 - ``` 240 - 241 - ### Sync Workflow 242 - 243 - Keep local notes in sync with leaflet: 244 - 245 - ```sh 246 - # Pull latest changes from leaflet 247 - noteleaf pub pull 248 - 249 - # Make local edits 250 - noteleaf note edit 123 251 - 252 - # Push changes back 253 - noteleaf pub patch 123 254 - 255 - # Check sync status 256 - noteleaf pub list --published 257 - ``` 258 - 259 - ### Draft Management 260 - 261 - Work with drafts before publishing: 262 - 263 - ```sh 264 - # Create drafts 265 - noteleaf pub post 10 --draft 266 - noteleaf pub post 11 --draft 267 - noteleaf pub post 12 --draft 268 - 269 - # View all drafts 270 - noteleaf pub list --draft 271 - 272 - # Edit a draft locally 273 - noteleaf note edit 10 274 - 275 - # Update on leaflet 276 - noteleaf pub patch 10 277 - 278 - # Promote draft to published (re-post without --draft) 279 - noteleaf pub post 10 280 - ``` 281 - 282 - ### Batch Publishing 283 - 284 - Publish multiple notes at once: 285 - 286 - ```sh 287 - # Create several notes 288 - noteleaf note create "Post 1" "Content 1" 289 - noteleaf note create "Post 2" "Content 2" 290 - noteleaf note create "Post 3" "Content 3" 291 - 292 - # Get note IDs 293 - noteleaf note list --static 294 - 295 - # Publish all at once 296 - noteleaf pub push 50 51 52 297 - 298 - # Or as drafts 299 - noteleaf pub push 50 51 52 --draft 300 - ``` 301 - 302 - ### Review Before Publishing 303 - 304 - Always preview and validate before publishing: 305 - 306 - ```sh 307 - # Validate markdown conversion 308 - noteleaf pub post 99 --validate 309 - 310 - # Preview the output 311 - noteleaf pub post 99 --preview 312 - 313 - # If everything looks good, publish 314 - noteleaf pub post 99 315 - ``` 316 - 317 - ### Cross-Platform Editing 318 - 319 - Edit on leaflet.pub, sync to local: 320 - 321 - ```sh 322 - # Pull changes from leaflet 323 - noteleaf pub pull 324 - 325 - # View what changed 326 - noteleaf pub list --interactive 327 - 328 - # Make additional edits locally 329 - noteleaf note edit 123 330 - 331 - # Push updates back 332 - noteleaf pub patch 123 333 - ``` 334 - 335 - ### Status Monitoring 336 - 337 - Check authentication and publication status: 338 - 339 - ```sh 340 - # Check auth status 341 - noteleaf pub status 342 - 343 - # List published documents 344 - noteleaf pub list --published 345 - 346 - # Count publications 347 - noteleaf pub list --published --static | wc -l 348 - ``` 349 - 350 - ## Troubleshooting 351 - 352 - ### Re-authenticate 353 - 354 - If authentication expires: 355 - 356 - ```sh 357 - noteleaf pub auth username.bsky.social 358 - ``` 359 - 360 - ### Check Status 361 - 362 - Verify connection: 363 - 364 - ```sh 365 - noteleaf pub status 366 - ``` 367 - 368 - ### Force Pull 369 - 370 - Re-sync all documents: 371 - 372 - ```sh 373 - noteleaf pub pull 374 - ``` 375 - 376 - ### Validate Before Publishing 377 - 378 - If publishing fails, validate first: 379 - 380 - ```sh 381 - noteleaf pub post 123 --validate 382 - ``` 383 - 384 - Check for markdown formatting issues that might not convert properly. 385 - 386 - ## Integration with Notes 387 - 388 - ### Publishing Flow 389 - 390 - ```sh 391 - # Create note locally 392 - noteleaf note create "Article Title" --interactive 393 - 394 - # Add tags for organization 395 - noteleaf note tag 1 --add published,blog 396 - 397 - # Publish to leaflet 398 - noteleaf pub post 1 399 - 400 - # Continue editing locally 401 - noteleaf note edit 1 402 - 403 - # Sync updates 404 - noteleaf pub patch 1 405 - ``` 406 - 407 - ### Import from Leaflet 408 - 409 - ```sh 410 - # Pull from leaflet 411 - noteleaf pub pull 412 - 413 - # View imported notes 414 - noteleaf pub list 415 - 416 - # Edit locally 417 - noteleaf note edit 123 418 - 419 - # Continue working with standard note commands 420 - noteleaf note read 123 421 - noteleaf note tag 123 --add imported 422 - ``` 423 - 424 - ## Advanced Usage 425 - 426 - ### Selective Publishing 427 - 428 - Publish only specific notes with a tag: 429 - 430 - ```sh 431 - # Tag notes for publication 432 - noteleaf note tag 10 --add ready-to-publish 433 - noteleaf note tag 11 --add ready-to-publish 434 - 435 - # List tagged notes 436 - noteleaf note list --tags ready-to-publish 437 - 438 - # Publish those notes 439 - noteleaf pub push 10 11 440 - ``` 441 - 442 - ### Draft Review Cycle 443 - 444 - ```sh 445 - # Publish drafts 446 - noteleaf pub push 1 2 3 --draft 447 - 448 - # Review on leaflet.pub in browser 449 - # Make edits locally based on feedback 450 - 451 - # Update drafts 452 - noteleaf pub push 1 2 3 453 - 454 - # When ready, publish (create as non-drafts) 455 - noteleaf pub post 1 456 - noteleaf pub post 2 457 - noteleaf pub post 3 458 - ``` 459 - 460 - ### Publication Archive 461 - 462 - Keep track of published work: 463 - 464 - ```sh 465 - # Tag published notes 466 - noteleaf note tag 123 --add published,2024,blog 467 - 468 - # List all published notes 469 - noteleaf note list --tags published 470 - 471 - # Archive old publications 472 - noteleaf note archive 123 473 - ``` 474 - 475 - ## Notes 476 - 477 - - Authentication tokens are stored in the configuration file 478 - - Notes are matched by their leaflet record key (rkey) 479 - - The `push` command intelligently chooses between `post` and `patch` 480 - - Draft status is preserved when patching existing documents 481 - - Use `--preview` and `--validate` flags to test before publishing 482 - - Pull regularly to stay synced with changes made on leaflet.pub
-288
website/docs/examples/tasks.md
··· 1 - # Task Examples 2 - 3 - Examples of common task management workflows using Noteleaf. 4 - 5 - ## Basic Task Management 6 - 7 - ### Create a Simple Task 8 - 9 - ```sh 10 - noteleaf task add "Buy groceries" 11 - ``` 12 - 13 - ### Create Task with Priority 14 - 15 - ```sh 16 - noteleaf task add "Fix critical bug" --priority urgent 17 - noteleaf task add "Update documentation" --priority low 18 - ``` 19 - 20 - ### Create Task with Project 21 - 22 - ```sh 23 - noteleaf task add "Design new homepage" --project website 24 - noteleaf task add "Refactor auth service" --project backend 25 - ``` 26 - 27 - ### Create Task with Due Date 28 - 29 - ```sh 30 - noteleaf task add "Submit report" --due 2024-12-31 31 - noteleaf task add "Review PRs" --due tomorrow 32 - ``` 33 - 34 - ### Create Task with Tags 35 - 36 - ```sh 37 - noteleaf task add "Write blog post" --tags writing,blog 38 - noteleaf task add "Server maintenance" --tags ops,backend,infra 39 - ``` 40 - 41 - ### Create Task with Context 42 - 43 - ```sh 44 - noteleaf task add "Call client" --context phone 45 - noteleaf task add "Deploy to production" --context office 46 - ``` 47 - 48 - ### Create Task with All Attributes 49 - 50 - ```sh 51 - noteleaf task add "Launch marketing campaign" \ 52 - --project marketing \ 53 - --priority high \ 54 - --due 2024-06-15 \ 55 - --tags campaign,social \ 56 - --context office 57 - ``` 58 - 59 - ## Viewing Tasks 60 - 61 - ### List All Tasks 62 - 63 - Interactive mode: 64 - 65 - ```sh 66 - noteleaf task list 67 - ``` 68 - 69 - Static output: 70 - 71 - ```sh 72 - noteleaf task list --static 73 - ``` 74 - 75 - ### Filter by Status 76 - 77 - ```sh 78 - noteleaf task list --status pending 79 - noteleaf task list --status completed 80 - ``` 81 - 82 - ### Filter by Priority 83 - 84 - ```sh 85 - noteleaf task list --priority high 86 - noteleaf task list --priority urgent 87 - ``` 88 - 89 - ### Filter by Project 90 - 91 - ```sh 92 - noteleaf task list --project website 93 - noteleaf task list --project backend 94 - ``` 95 - 96 - ### Filter by Tags 97 - 98 - ```sh 99 - noteleaf task list --tags urgent,bug 100 - ``` 101 - 102 - ### View Task Details 103 - 104 - ```sh 105 - noteleaf task view 1 106 - ``` 107 - 108 - ## Updating Tasks 109 - 110 - ### Mark Task as Done 111 - 112 - ```sh 113 - noteleaf task done 1 114 - ``` 115 - 116 - ### Update Task Priority 117 - 118 - ```sh 119 - noteleaf task update 1 --priority high 120 - ``` 121 - 122 - ### Update Task Project 123 - 124 - ```sh 125 - noteleaf task update 1 --project website 126 - ``` 127 - 128 - ### Add Tags to Task 129 - 130 - ```sh 131 - noteleaf task update 1 --add-tags backend,api 132 - ``` 133 - 134 - ### Remove Tags from Task 135 - 136 - ```sh 137 - noteleaf task update 1 --remove-tags urgent 138 - ``` 139 - 140 - ### Edit Task Interactively 141 - 142 - Opens task in your editor: 143 - 144 - ```sh 145 - noteleaf task edit 1 146 - ``` 147 - 148 - ## Time Tracking 149 - 150 - ### Start Time Tracking 151 - 152 - ```sh 153 - noteleaf task start 1 154 - ``` 155 - 156 - ### Stop Time Tracking 157 - 158 - ```sh 159 - noteleaf task stop 1 160 - ``` 161 - 162 - ### View Timesheet 163 - 164 - All entries: 165 - 166 - ```sh 167 - noteleaf task timesheet 168 - ``` 169 - 170 - Filtered by date: 171 - 172 - ```sh 173 - noteleaf task timesheet --from 2024-01-01 --to 2024-01-31 174 - ``` 175 - 176 - Filtered by project: 177 - 178 - ```sh 179 - noteleaf task timesheet --project website 180 - ``` 181 - 182 - ## Project Management 183 - 184 - ### List All Projects 185 - 186 - ```sh 187 - noteleaf task projects 188 - ``` 189 - 190 - ### View Tasks in Project 191 - 192 - ```sh 193 - noteleaf task list --project website 194 - ``` 195 - 196 - ## Tag Management 197 - 198 - ### List All Tags 199 - 200 - ```sh 201 - noteleaf task tags 202 - ``` 203 - 204 - ### View Tasks with Tag 205 - 206 - ```sh 207 - noteleaf task list --tags urgent 208 - ``` 209 - 210 - ## Context Management 211 - 212 - ### List All Contexts 213 - 214 - ```sh 215 - noteleaf task contexts 216 - ``` 217 - 218 - ### View Tasks in Context 219 - 220 - ```sh 221 - noteleaf task list --context office 222 - ``` 223 - 224 - ## Advanced Workflows 225 - 226 - ### Daily Planning 227 - 228 - View today's tasks: 229 - 230 - ```sh 231 - noteleaf task list --due today 232 - ``` 233 - 234 - View overdue tasks: 235 - 236 - ```sh 237 - noteleaf task list --due overdue 238 - ``` 239 - 240 - ### Weekly Review 241 - 242 - View completed tasks this week: 243 - 244 - ```sh 245 - noteleaf task list --status completed --from monday 246 - ``` 247 - 248 - View pending high-priority tasks: 249 - 250 - ```sh 251 - noteleaf task list --status pending --priority high 252 - ``` 253 - 254 - ### Project Focus 255 - 256 - List all tasks for a project, sorted by priority: 257 - 258 - ```sh 259 - noteleaf task list --project website --sort priority 260 - ``` 261 - 262 - ### Bulk Operations 263 - 264 - Mark multiple tasks as done: 265 - 266 - ```sh 267 - noteleaf task done 1 2 3 4 268 - ``` 269 - 270 - Delete multiple tasks: 271 - 272 - ```sh 273 - noteleaf task delete 5 6 7 274 - ``` 275 - 276 - ## Task Deletion 277 - 278 - ### Delete a Task 279 - 280 - ```sh 281 - noteleaf task delete 1 282 - ``` 283 - 284 - ### Delete with Confirmation 285 - 286 - ```sh 287 - noteleaf task delete 1 --confirm 288 - ```
+204
website/docs/integrations/openlibrary.md
··· 1 + --- 2 + title: Open Library API 3 + sidebar_label: Open Library 4 + sidebar_position: 1 5 + description: Book metadata via Open Library API integration. 6 + --- 7 + 8 + # Open Library API 9 + 10 + Noteleaf integrates with [Open Library](https://openlibrary.org) to fetch book metadata, search for books, and enrich your reading list. 11 + 12 + ## Overview 13 + 14 + Open Library provides: 15 + 16 + - Book search by title, author, ISBN 17 + - Work and edition metadata 18 + - Author information 19 + - Cover images 20 + - Subject classifications 21 + - Publication details 22 + 23 + ## Configuration 24 + 25 + No API key required. Open Library is a free, open API service. 26 + 27 + Optional user agent configuration is handled automatically: 28 + 29 + ```toml 30 + # .noteleaf.conf.toml 31 + # No configuration needed for Open Library 32 + ``` 33 + 34 + ## Rate Limiting 35 + 36 + Open Library enforces rate limits: 37 + 38 + - 180 requests per minute 39 + - 3 requests per second 40 + - Burst limit: 5 requests 41 + 42 + Noteleaf automatically manages rate limiting to stay within these boundaries. 43 + 44 + ## Book Search 45 + 46 + Search for books from the command line: 47 + 48 + ```sh 49 + noteleaf book search "Design Patterns" 50 + noteleaf book search "Neal Stephenson" 51 + ``` 52 + 53 + Interactive selection shows: 54 + 55 + - Title 56 + - Author(s) 57 + - First publication year 58 + - Edition count 59 + - Publisher information 60 + 61 + ## Book Metadata 62 + 63 + When adding a book, Noteleaf fetches: 64 + 65 + - Title 66 + - Author names 67 + - Publication year 68 + - Edition information 69 + - Subjects/genres 70 + - Description (when available) 71 + - Cover ID 72 + 73 + ## API Endpoints 74 + 75 + ### Search Endpoint 76 + 77 + ``` 78 + GET https://openlibrary.org/search.json 79 + ``` 80 + 81 + Parameters: 82 + 83 + - `q`: Search query 84 + - `offset`: Pagination offset 85 + - `limit`: Results per page 86 + - `fields`: Requested fields 87 + 88 + ### Work Endpoint 89 + 90 + ``` 91 + GET https://openlibrary.org/works/{work_key}.json 92 + ``` 93 + 94 + Returns detailed work information including authors, description, subjects, and covers. 95 + 96 + ## Data Mapping 97 + 98 + Open Library data maps to Noteleaf book fields: 99 + 100 + | Open Library | Noteleaf Field | 101 + |--------------|----------------| 102 + | title | Title | 103 + | author_name | Author | 104 + | first_publish_year | Notes (included) | 105 + | edition_count | Notes (included) | 106 + | publisher | Notes (included) | 107 + | subject | Notes (included) | 108 + | cover_i | Notes (cover ID) | 109 + 110 + ## Example API Response 111 + 112 + Search result document: 113 + 114 + ```json 115 + { 116 + "key": "/works/OL45804W", 117 + "title": "Design Patterns", 118 + "author_name": ["Erich Gamma", "Richard Helm"], 119 + "first_publish_year": 1994, 120 + "edition_count": 23, 121 + "isbn": ["0201633612", "9780201633610"], 122 + "publisher": ["Addison-Wesley"], 123 + "subject": ["Software design", "Object-oriented programming"], 124 + "cover_i": 8644882 125 + } 126 + ``` 127 + 128 + ## Limitations 129 + 130 + ### No Direct Page Count 131 + 132 + Open Library doesn't consistently provide page counts in search results. Use the interactive editor to add page counts manually if needed. 133 + 134 + ### Author Keys vs Names 135 + 136 + Work endpoints return author keys (`/authors/OL123A`) rather than full names. Noteleaf displays available author names from search results. 137 + 138 + ### Cover Images 139 + 140 + Cover IDs are stored but not automatically downloaded. Future versions may support local cover image caching. 141 + 142 + ## Error Handling 143 + 144 + ### Network Issues 145 + 146 + ```sh 147 + noteleaf book search "query" 148 + # Error: failed to connect to Open Library 149 + ``` 150 + 151 + Check internet connection and Open Library status. 152 + 153 + ### Rate Limit Exceeded 154 + 155 + Noteleaf automatically waits when approaching rate limits. If you see delays, this is normal behavior. 156 + 157 + ### No Results 158 + 159 + ```sh 160 + noteleaf book search "very obscure title" 161 + # No results found 162 + ``` 163 + 164 + Try: 165 + 166 + - Different search terms 167 + - Author names instead of titles 168 + - ISBNs for specific editions 169 + 170 + ## API Service Architecture 171 + 172 + Implementation in `internal/services/services.go`: 173 + 174 + ```go 175 + type BookService struct { 176 + client *http.Client 177 + limiter *rate.Limiter 178 + baseURL string 179 + } 180 + ``` 181 + 182 + Features: 183 + 184 + - Automatic rate limiting 185 + - Context-aware requests 186 + - Proper error handling 187 + - Timeout management (30s) 188 + 189 + ## Custom User Agent 190 + 191 + Noteleaf identifies itself to Open Library: 192 + 193 + ``` 194 + User-Agent: Noteleaf/v{version} (contact: info@stormlightlabs.org) 195 + ``` 196 + 197 + This helps Open Library track API usage and contact developers if needed. 198 + 199 + ## Resources 200 + 201 + - [Open Library API Documentation](https://openlibrary.org/dev/docs/api/books) 202 + - [Open Library Search](https://openlibrary.org/dev/docs/api/search) 203 + - [Open Library Covers](https://openlibrary.org/dev/docs/api/covers) 204 + - [Rate Limiting Policy](https://openlibrary.org/developers/api)
+164
website/docs/integrations/overview.md
··· 1 + --- 2 + title: External Integrations 3 + sidebar_label: Overview 4 + sidebar_position: 1 5 + description: Overview of external service integrations. 6 + --- 7 + 8 + # External Integrations 9 + 10 + Noteleaf integrates with external services to enrich your productivity workflow and extend functionality beyond local storage. 11 + 12 + ## Available Integrations 13 + 14 + ### Open Library API 15 + 16 + Free book metadata service for building your reading list. 17 + 18 + **Features:** 19 + 20 + - Search books by title, author, ISBN 21 + - Fetch metadata (author, year, subjects) 22 + - Edition and publication information 23 + - No API key required 24 + 25 + **Use Cases:** 26 + 27 + - Adding books to reading list 28 + - Enriching book metadata 29 + - Discovering related works 30 + 31 + See [Open Library API](./openlibrary.md) for details. 32 + 33 + ### Leaflet.pub 34 + 35 + Decentralized publishing platform built on AT Protocol. 36 + 37 + **Features:** 38 + 39 + - Publish notes as structured documents 40 + - Pull existing documents into local notes 41 + - Update published content 42 + - Manage drafts and publications 43 + 44 + **Use Cases:** 45 + 46 + - Blog publishing from terminal 47 + - Long-form content management 48 + - Decentralized content ownership 49 + 50 + See [Leaflet.pub section](../leaflet/intro.md) for details. 51 + 52 + ### AT Protocol (Bluesky) 53 + 54 + Authentication and identity via AT Protocol network. 55 + 56 + **Features:** 57 + 58 + - Decentralized identity (DID) 59 + - Session management 60 + - Token refresh 61 + - Secure authentication 62 + 63 + **Use Cases:** 64 + 65 + - Leaflet.pub authentication 66 + - Portable identity across services 67 + - Content verification 68 + 69 + See [Authentication](../leaflet/authentication.md) for details. 70 + 71 + ## Integration Architecture 72 + 73 + ### Service Layer 74 + 75 + External integrations live in `internal/services/`: 76 + 77 + - `services.go` - Open Library API client 78 + - `atproto.go` - AT Protocol authentication 79 + - `http.go` - HTTP utilities and rate limiting 80 + 81 + ### Rate Limiting 82 + 83 + All external services use rate limiting to respect API quotas: 84 + 85 + - Open Library: 3 requests/second 86 + - AT Protocol: Per PDS configuration 87 + 88 + Rate limiters are built-in and automatic. 89 + 90 + ### Error Handling 91 + 92 + Services implement consistent error handling: 93 + 94 + - Network errors 95 + - Rate limit exceeded 96 + - Authentication failures 97 + - Invalid responses 98 + 99 + Errors propagate to user with actionable messages. 100 + 101 + ## Configuration 102 + 103 + Integration configuration in `.noteleaf.conf.toml`: 104 + 105 + ```toml 106 + # Open Library (no configuration needed) 107 + # book_api_key = "" # Reserved for future use 108 + 109 + # AT Protocol / Leaflet.pub 110 + atproto_handle = "username.bsky.social" 111 + atproto_did = "did:plc:..." 112 + atproto_pds_url = "https://bsky.social" 113 + atproto_access_jwt = "..." 114 + atproto_refresh_jwt = "..." 115 + ``` 116 + 117 + See [Configuration](../Configuration.md) for all options. 118 + 119 + ## Offline Support 120 + 121 + Noteleaf works fully offline for local data. Integrations are optional enhancements: 122 + 123 + - Books can be added manually without Open Library 124 + - Notes exist locally without Leaflet.pub 125 + - Tasks and media work without any external service 126 + 127 + External services enhance but don't require connectivity. 128 + 129 + ## Privacy and Data 130 + 131 + ### Data Sent 132 + 133 + **Open Library:** 134 + 135 + - Search queries 136 + - Work/edition IDs 137 + 138 + **AT Protocol:** 139 + 140 + - Handle/DID 141 + - Published note content 142 + - Authentication credentials 143 + 144 + ### Data Stored Locally 145 + 146 + - API responses (cached) 147 + - Session tokens 148 + - Publication metadata 149 + 150 + ### No Tracking 151 + 152 + Noteleaf does not: 153 + 154 + - Track usage 155 + - Send analytics 156 + - Share data with third parties 157 + - Require accounts (except for publishing) 158 + 159 + ## Resources 160 + 161 + - [Open Library API Documentation](https://openlibrary.org/developers/api) 162 + - [AT Protocol Docs](https://atproto.com) 163 + - [Leaflet.pub](https://leaflet.pub) 164 + - [Bluesky](https://bsky.app)
-47
website/docs/intro.md
··· 1 - --- 2 - sidebar_position: 1 3 - --- 4 - 5 - # Tutorial Intro 6 - 7 - Let's discover **Docusaurus in less than 5 minutes**. 8 - 9 - ## Getting Started 10 - 11 - Get started by **creating a new site**. 12 - 13 - Or **try Docusaurus immediately** with **[docusaurus.new](https://docusaurus.new)**. 14 - 15 - ### What you'll need 16 - 17 - - [Node.js](https://nodejs.org/en/download/) version 20.0 or above: 18 - - When installing Node.js, you are recommended to check all checkboxes related to dependencies. 19 - 20 - ## Generate a new site 21 - 22 - Generate a new Docusaurus site using the **classic template**. 23 - 24 - The classic template will automatically be added to your project after you run the command: 25 - 26 - ```bash 27 - npm init docusaurus@latest my-website classic 28 - ``` 29 - 30 - You can type this command into Command Prompt, Powershell, Terminal, or any other integrated terminal of your code editor. 31 - 32 - The command also installs all necessary dependencies you need to run Docusaurus. 33 - 34 - ## Start your site 35 - 36 - Run the development server: 37 - 38 - ```bash 39 - cd my-website 40 - npm run start 41 - ``` 42 - 43 - The `cd` command changes the directory you're working with. In order to work with your newly created Docusaurus site, you'll need to navigate the terminal there. 44 - 45 - The `npm run start` command builds your website locally and serves it through a development server, ready for you to view at http://localhost:3000/. 46 - 47 - Open `docs/intro.md` (this page) and edit some lines: the site **reloads automatically** and displays your changes.
+9
website/docs/leaflet/_category_.json
··· 1 + { 2 + "label": "Leaflet Publishing", 3 + "position": 5, 4 + "link": { 5 + "type": "generated-index", 6 + "title": "Leaflet Publishing", 7 + "description": "Publish and sync documents with leaflet.pub and the AT Protocol." 8 + } 9 + }
+41
website/docs/leaflet/authentication.md
··· 1 + --- 2 + title: Authentication and Identity 3 + sidebar_label: Auth & Identity 4 + description: AT Protocol authentication, security, and session handling. 5 + sidebar_position: 8 6 + --- 7 + 8 + # Authentication and Identity 9 + 10 + ## AT Protocol Authentication 11 + 12 + Noteleaf uses AT Protocol's authentication system: 13 + 14 + 1. **Handle Resolution**: Your handle (e.g., `username.bsky.social`) is resolved to a DID (Decentralized Identifier) 15 + 2. **Session Creation**: Authenticate with your app password to create a session 16 + 3. **Session Token**: Noteleaf stores the session token for future requests 17 + 4. **Token Refresh**: Sessions are refreshed automatically when they expire 18 + 19 + ## Security Considerations 20 + 21 + **Use app passwords**: Never use your main BlueSky password with third-party tools. App passwords can be revoked without affecting your account. 22 + 23 + **Token storage**: Session tokens are stored locally in the Noteleaf database. Protect your database file. 24 + 25 + **Revocation**: If compromised, revoke the app password at [bsky.app/settings/app-passwords](https://bsky.app/settings/app-passwords). 26 + 27 + ## Session Management 28 + 29 + **Check status**: 30 + 31 + ```sh 32 + noteleaf pub status 33 + ``` 34 + 35 + **Re-authenticate**: 36 + 37 + ```sh 38 + noteleaf pub auth 39 + ``` 40 + 41 + Sessions typically last 2-4 hours before requiring refresh. Noteleaf handles refresh automatically, but if authentication fails, run `pub auth` again.
+85
website/docs/leaflet/examples.md
··· 1 + --- 2 + title: Publishing Examples 3 + sidebar_label: Examples 4 + description: End-to-end examples for posting, drafting, and syncing. 5 + sidebar_position: 6 6 + --- 7 + 8 + # Publishing Examples 9 + 10 + ## Publishing a Blog Post 11 + 12 + **Write the post locally**: 13 + 14 + ```sh 15 + noteleaf note create "Understanding AT Protocol" --editor 16 + ``` 17 + 18 + Write in markdown, save, and close editor. 19 + 20 + **Preview the conversion**: 21 + 22 + ```sh 23 + noteleaf pub post <note-id> --preview 24 + ``` 25 + 26 + Review the output to ensure formatting is correct. 27 + 28 + **Publish**: 29 + 30 + ```sh 31 + noteleaf pub post <note-id> 32 + ``` 33 + 34 + **Update later**: 35 + 36 + ```sh 37 + noteleaf note edit <note-id> 38 + # Make changes 39 + noteleaf pub patch <note-id> 40 + ``` 41 + 42 + ## Draft Workflow 43 + 44 + **Create draft**: 45 + 46 + ```sh 47 + noteleaf note create "Work in Progress" --editor 48 + noteleaf pub post <note-id> --draft 49 + ``` 50 + 51 + **Iterate locally**: 52 + 53 + ```sh 54 + noteleaf note edit <note-id> 55 + noteleaf pub patch <note-id> # Updates draft 56 + ``` 57 + 58 + **Publish when ready**: Use leaflet.pub web interface to change draft to published status (CLI command coming in future versions). 59 + 60 + ## Syncing Existing Content 61 + 62 + **Pull all leaflet documents**: 63 + 64 + ```sh 65 + noteleaf pub pull 66 + ``` 67 + 68 + **List synced documents**: 69 + 70 + ```sh 71 + noteleaf pub list 72 + ``` 73 + 74 + **Read a synced document**: 75 + 76 + ```sh 77 + noteleaf pub read <note-id> 78 + ``` 79 + 80 + **Edit locally and push updates**: 81 + 82 + ```sh 83 + noteleaf note edit <note-id> 84 + noteleaf pub patch <note-id> 85 + ```
+53
website/docs/leaflet/getting-started.md
··· 1 + --- 2 + title: Getting Started with Leaflet 3 + sidebar_label: Getting Started 4 + description: Prerequisites, app passwords, and authentication commands. 5 + sidebar_position: 2 6 + --- 7 + 8 + # Getting Started with Leaflet 9 + 10 + ## Prerequisites 11 + 12 + - Noteleaf installed and configured 13 + - A BlueSky & [Leaflet](https://leaflet.pub) account (create at [bsky.app](https://bsky.app)) 14 + - App password for authentication 15 + 16 + ## Creating an App Password 17 + 18 + For security, Noteleaf uses app passwords instead of your main BlueSky password: 19 + 20 + 1. Go to [bsky.app/settings/app-passwords](https://bsky.app/settings/app-passwords) 21 + 2. Click "Add App Password" 22 + 3. Name it "noteleaf" or similar 23 + 4. Copy the generated password (you won't see it again) 24 + 25 + ### Authentication 26 + 27 + Authenticate with your BlueSky handle and app password: 28 + 29 + ```sh 30 + noteleaf pub auth username.bsky.social 31 + ``` 32 + 33 + You'll be prompted for the app password. Alternatively, provide it via flag: 34 + 35 + ```sh 36 + noteleaf pub auth username.bsky.social --password <app-password> 37 + ``` 38 + 39 + **Re-authentication**: If your session expires, run `pub auth` again. Noteleaf remembers your last authenticated handle, so you can just run: 40 + 41 + ```sh 42 + noteleaf pub auth 43 + ``` 44 + 45 + ### Check Authentication Status 46 + 47 + Verify you're authenticated: 48 + 49 + ```sh 50 + noteleaf pub status 51 + ``` 52 + 53 + Shows your handle and session state.
+36
website/docs/leaflet/intro.md
··· 1 + --- 2 + title: Leaflet.pub Introduction 3 + sidebar_label: Introduction 4 + description: Understand leaflet.pub and how Noteleaf integrates with it. 5 + sidebar_position: 1 6 + --- 7 + 8 + # Leaflet.pub Introduction 9 + 10 + ## What is Leaflet.pub? 11 + 12 + [Leaflet.pub](https://leaflet.pub) is a decentralized publishing platform built on the AT Protocol (the same protocol that powers BlueSky). It allows you to publish long-form content as structured documents while maintaining ownership and control of your data through decentralized identity. 13 + 14 + ## AT Protocol and Decentralized Publishing 15 + 16 + AT Protocol provides: 17 + 18 + **Portable Identity**: Your identity (DID) is separate from any single service. You own your content and can move it between providers. 19 + 20 + **Verifiable Data**: All documents are content-addressed and cryptographically signed, ensuring authenticity and preventing tampering. 21 + 22 + **Interoperability**: Content published to leaflet.pub can be discovered and consumed by any AT Protocol-compatible client. 23 + 24 + **Decentralized Storage**: Data is stored in personal data repositories (PDSs) under your control, not locked in a proprietary platform. 25 + 26 + ## How Noteleaf Integrates with Leaflet 27 + 28 + Noteleaf can act as a leaflet.pub client, allowing you to: 29 + 30 + 1. **Authenticate** with your BlueSky/AT Protocol identity 31 + 2. **Pull** existing documents from leaflet.pub into local notes 32 + 3. **Publish** local notes as new leaflet documents 33 + 4. **Update** previously published documents with changes 34 + 5. **Manage** drafts and published content from the command line 35 + 36 + This integration lets you write locally in markdown, manage content alongside tasks and research notes, and publish to a decentralized platform when ready.
+57
website/docs/leaflet/listing.md
··· 1 + --- 2 + title: Listing and Reading Publications 3 + sidebar_label: Listing & Reading 4 + description: Browse leaflet-backed notes and read content locally. 5 + sidebar_position: 7 6 + --- 7 + 8 + # Listing and Reading Publications 9 + 10 + ## List Published Documents 11 + 12 + **All leaflet-synced notes**: 13 + 14 + ```sh 15 + noteleaf pub list 16 + ``` 17 + 18 + **Only published documents**: 19 + 20 + ```sh 21 + noteleaf pub list --published 22 + ``` 23 + 24 + **Only drafts**: 25 + 26 + ```sh 27 + noteleaf pub list --draft 28 + ``` 29 + 30 + **Interactive browser**: 31 + 32 + ```sh 33 + noteleaf pub list --interactive 34 + ``` 35 + 36 + Navigate with arrow keys, press Enter to read, `q` to quit. 37 + 38 + ## Reading a Publication 39 + 40 + **Read specific document**: 41 + 42 + ```sh 43 + noteleaf pub read 123 44 + ``` 45 + 46 + The identifier can be: 47 + 48 + - Note ID (e.g., `123`) 49 + - Leaflet record key (rkey, e.g., `3jxx...`) 50 + 51 + **Read newest publication**: 52 + 53 + ```sh 54 + noteleaf pub read 55 + ``` 56 + 57 + Omitting the identifier shows the most recently published document.
+16
website/docs/leaflet/publications.md
··· 1 + --- 2 + title: Leaflet Publications 3 + sidebar_label: Publications 4 + description: How publications work and how Noteleaf interacts with them. 5 + sidebar_position: 3 6 + --- 7 + 8 + # Leaflet Publications 9 + 10 + ## What is a Publication? 11 + 12 + In leaflet.pub, a publication is a collection of documents. While Noteleaf doesn't support management commands, documents you create are associated with your default publication. 13 + 14 + Future versions may support: 15 + 16 + - Switching between publications
+95
website/docs/leaflet/rich-text.md
··· 1 + --- 2 + title: Leaflet Rich Text and Blocks 3 + sidebar_label: Rich Text 4 + description: How markdown maps to leaflet blocks and formatting. 5 + sidebar_position: 5 6 + --- 7 + 8 + # Leaflet Rich Text and Blocks 9 + 10 + ## Document Structure 11 + 12 + Leaflet documents consist of blocksโ€”discrete content units: 13 + 14 + **Text Blocks**: Paragraphs of formatted text 15 + **Header Blocks**: Section titles (level 1-6) 16 + **Code Blocks**: Syntax-highlighted code with language annotation 17 + **Quote Blocks**: Blockquotes for citations 18 + **List Blocks**: Ordered or unordered lists 19 + **Rule Blocks**: Horizontal rules for visual separation 20 + 21 + ## Text Formatting 22 + 23 + Text within blocks can have inline formatting called facets: 24 + 25 + **Bold**: `**bold text**` โ†’ Bold facet 26 + **Italic**: `*italic text*` โ†’ Italic facet 27 + **Code**: `` `inline code` `` โ†’ Code facet 28 + **Links**: `[text](url)` โ†’ Link facet with URL 29 + **Strikethrough**: `~~struck~~` โ†’ Strikethrough facet 30 + 31 + Multiple formats can be combined: 32 + 33 + ```markdown 34 + **bold and *italic* text with [a link](https://example.com)** 35 + ``` 36 + 37 + ## Code Blocks 38 + 39 + Code blocks preserve language information for syntax highlighting: 40 + 41 + ````markdown 42 + ```python 43 + def hello(): 44 + print("Hello, leaflet!") 45 + ``` 46 + ```` 47 + 48 + Converts to a code block with language="python". 49 + 50 + Supported languages: Any language identifier is preserved, but rendering depends on leaflet.pub's syntax highlighter support. 51 + 52 + ## Blockquotes 53 + 54 + Markdown blockquotes become quote blocks: 55 + 56 + ```markdown 57 + > This is a quote from another source. 58 + > It can span multiple lines. 59 + ``` 60 + 61 + Nested blockquotes are flattened (leaflet doesn't support nesting). 62 + 63 + ## Lists 64 + 65 + Both ordered and unordered lists are supported: 66 + 67 + ```markdown 68 + - Unordered item 1 69 + - Unordered item 2 70 + - Nested item 71 + 72 + 1. Ordered item 1 73 + 2. Ordered item 2 74 + 1. Nested ordered item 75 + ``` 76 + 77 + Nesting is preserved up to leaflet's limits. 78 + 79 + ## Horizontal Rules 80 + 81 + Markdown horizontal rules become rule blocks: 82 + 83 + ```markdown 84 + --- 85 + ``` 86 + 87 + Use for section breaks. 88 + 89 + ## Images and Media 90 + 91 + **Current status**: Image support is not yet implemented in the Noteleaf-to-leaflet converter. 92 + 93 + **Future plans**: Images will be uploaded to blob storage and embedded in documents with image blocks. 94 + 95 + **Workaround**: For now, images in markdown are either skipped or converted to links.
+147
website/docs/leaflet/workflow.md
··· 1 + --- 2 + title: Publishing Workflow 3 + sidebar_label: Workflow 4 + description: Post, patch, drafts, pulling, and syncing documents. 5 + sidebar_position: 4 6 + --- 7 + 8 + # Publishing Workflow 9 + 10 + ## Converting Notes to Leaflet Documents 11 + 12 + Noteleaf converts markdown notes to leaflet's rich text block format: 13 + 14 + **Supported Markdown Features**: 15 + 16 + - Headers (`#`, `##`, `###`, etc.) 17 + - Paragraphs 18 + - Bold (`**bold**`) 19 + - Italic (`*italic*`) 20 + - Code (`inline code`) 21 + - Strikethrough (`~~text~~`) 22 + - Links (`[text](url)`) 23 + - Code blocks (` ```language ... ``` `) 24 + - Blockquotes (`> quote`) 25 + - Lists (ordered and unordered) 26 + - Horizontal rules (`---`) 27 + 28 + **Conversion Process**: 29 + 30 + 1. Parse markdown into AST (abstract syntax tree) 31 + 2. Convert AST nodes to leaflet block records 32 + 3. Process text formatting into facets 33 + 4. Validate document structure 34 + 5. Upload to leaflet.pub via AT Protocol 35 + 36 + ## Creating a New Document 37 + 38 + Publish a local note as a new leaflet document: 39 + 40 + ```sh 41 + noteleaf pub post 123 42 + ``` 43 + 44 + This: 45 + 46 + 1. Converts the note to leaflet format 47 + 2. Creates a new document on leaflet.pub 48 + 3. Links the note to the document (stores the rkey) 49 + 4. Marks the note as published 50 + 51 + **Create as draft**: 52 + 53 + ```sh 54 + noteleaf pub post 123 --draft 55 + ``` 56 + 57 + Drafts are saved to leaflet but not publicly visible until you publish them. 58 + 59 + **Preview before posting**: 60 + 61 + ```sh 62 + noteleaf pub post 123 --preview 63 + ``` 64 + 65 + Shows what the document will look like without actually posting. 66 + 67 + **Validate conversion**: 68 + 69 + ```sh 70 + noteleaf pub post 123 --validate 71 + ``` 72 + 73 + Checks if the markdown converts correctly to leaflet format without posting. 74 + 75 + **Save to file**: 76 + 77 + ```sh 78 + noteleaf pub post 123 --preview --output document.json 79 + noteleaf pub post 123 --preview --output document.txt --plaintext 80 + ``` 81 + 82 + ## Updating Published Documents 83 + 84 + Update an existing leaflet document from a local note: 85 + 86 + ```sh 87 + noteleaf pub patch 123 88 + ``` 89 + 90 + Requirements: 91 + 92 + - Note must have been previously posted or pulled from leaflet 93 + - Note must have a leaflet record key (rkey) in the database 94 + 95 + **Preserve draft/published status**: The `patch` command maintains the document's current status. If it's published, it stays published. If it's a draft, it stays a draft. 96 + 97 + **Preview changes**: 98 + 99 + ```sh 100 + noteleaf pub patch 123 --preview 101 + ``` 102 + 103 + **Validate before patching**: 104 + 105 + ```sh 106 + noteleaf pub patch 123 --validate 107 + ``` 108 + 109 + ## Managing Drafts 110 + 111 + **Create as draft**: 112 + 113 + ```sh 114 + noteleaf pub post 123 --draft 115 + ``` 116 + 117 + **Update draft**: 118 + 119 + ```sh 120 + noteleaf pub patch 123 121 + ``` 122 + 123 + **List drafts**: 124 + 125 + ```sh 126 + noteleaf pub list --draft 127 + ``` 128 + 129 + **Publish a draft**: Edit the draft on leaflet.pub or use the API to change status (command support coming in future versions). 130 + 131 + ## Pulling Documents from Leaflet 132 + 133 + Sync leaflet documents to local notes: 134 + 135 + ```sh 136 + noteleaf pub pull 137 + ``` 138 + 139 + This: 140 + 141 + 1. Authenticates with leaflet.pub 142 + 2. Fetches all documents in your repository 143 + 3. Creates new notes for documents not yet synced 144 + 4. Updates existing notes that have changed 145 + 146 + **Matching logic**: Notes are matched to leaflet documents by their record key (rkey) stored in the database. 147 + If a document doesn't have a corresponding note, a new one is created. If it does, the note is updated only if the content has changed (using CID for change detection).
-8
website/docs/manual/_category_.json
··· 1 - { 2 - "label": "CLI Reference", 3 - "position": 3, 4 - "link": { 5 - "type": "generated-index", 6 - "description": "Complete command-line reference for noteleaf" 7 - } 8 - }
-97
website/docs/manual/articles.md
··· 1 - --- 2 - id: articles 3 - title: Articles 4 - sidebar_position: 3 5 - description: Save and archive web articles 6 - --- 7 - 8 - ## article 9 - 10 - Save and archive web articles locally. 11 - 12 - Parse articles from supported websites, extract clean content, and save as 13 - both markdown and HTML. Maintains a searchable archive of articles with 14 - metadata including author, title, and publication date. 15 - 16 - ```bash 17 - noteleaf article 18 - ``` 19 - 20 - ### Subcommands 21 - 22 - #### add 23 - 24 - Parse and save article content from a supported website. 25 - 26 - The article will be parsed using domain-specific XPath rules and saved 27 - as both Markdown and HTML files. Article metadata is stored in the database. 28 - 29 - **Usage:** 30 - 31 - ```bash 32 - noteleaf article add <url> 33 - ``` 34 - 35 - #### list 36 - 37 - List saved articles with optional filtering. 38 - 39 - Use query to filter by title, or use flags for more specific filtering. 40 - 41 - **Usage:** 42 - 43 - ```bash 44 - noteleaf article list [query] [flags] 45 - ``` 46 - 47 - **Options:** 48 - 49 - ``` 50 - --author string Filter by author 51 - -l, --limit int Limit number of results (0 = no limit) 52 - ``` 53 - 54 - **Aliases:** ls 55 - 56 - #### view 57 - 58 - Display article metadata and summary. 59 - 60 - Shows article title, author, publication date, URL, and a brief content 61 - preview. Use 'read' command to view the full article content. 62 - 63 - **Usage:** 64 - 65 - ```bash 66 - noteleaf article view <id> 67 - ``` 68 - 69 - **Aliases:** show 70 - 71 - #### read 72 - 73 - Read the full markdown content of an article with beautiful formatting. 74 - 75 - This displays the complete article content using syntax highlighting and proper formatting. 76 - 77 - **Usage:** 78 - 79 - ```bash 80 - noteleaf article read <id> 81 - ``` 82 - 83 - #### remove 84 - 85 - Delete an article and its files permanently. 86 - 87 - Removes the article metadata from the database and deletes associated markdown 88 - and HTML files. This operation cannot be undone. 89 - 90 - **Usage:** 91 - 92 - ```bash 93 - noteleaf article remove <id> 94 - ``` 95 - 96 - **Aliases:** rm, delete 97 -
-115
website/docs/manual/books.md
··· 1 - --- 2 - id: books 3 - title: Books 4 - sidebar_position: 4 5 - description: Manage reading list and track progress 6 - --- 7 - 8 - ## book 9 - 10 - Track books and reading progress. 11 - 12 - Search Google Books API to add books to your reading list. Track which books 13 - you're reading, update progress percentages, and maintain a history of finished 14 - books. 15 - 16 - ```bash 17 - noteleaf media book 18 - ``` 19 - 20 - ### Subcommands 21 - 22 - #### add 23 - 24 - Search for books and add them to your reading list. 25 - 26 - By default, shows search results in a simple list format where you can select by number. 27 - Use the -i flag for an interactive interface with navigation keys. 28 - 29 - **Usage:** 30 - 31 - ```bash 32 - noteleaf media book add [search query...] [flags] 33 - ``` 34 - 35 - **Options:** 36 - 37 - ``` 38 - -i, --interactive Use interactive interface for book selection 39 - ``` 40 - 41 - #### list 42 - 43 - Display books in your reading list with progress indicators. 44 - 45 - Shows book titles, authors, and reading progress percentages. Filter by --all, 46 - --reading for books in progress, --finished for completed books, or --queued 47 - for books not yet started. Default shows queued books only. 48 - 49 - **Usage:** 50 - 51 - ```bash 52 - noteleaf media book list [--all|--reading|--finished|--queued] 53 - ``` 54 - 55 - #### reading 56 - 57 - Mark a book as currently reading. Use this when you start a book from your queue. 58 - 59 - **Usage:** 60 - 61 - ```bash 62 - noteleaf media book reading <id> 63 - ``` 64 - 65 - #### finished 66 - 67 - Mark a book as finished with current timestamp. Sets reading progress to 100%. 68 - 69 - **Usage:** 70 - 71 - ```bash 72 - noteleaf media book finished <id> 73 - ``` 74 - 75 - **Aliases:** read 76 - 77 - #### remove 78 - 79 - Remove a book from your reading list. Use this for books you no longer want to track. 80 - 81 - **Usage:** 82 - 83 - ```bash 84 - noteleaf media book remove <id> 85 - ``` 86 - 87 - **Aliases:** rm 88 - 89 - #### progress 90 - 91 - Set reading progress for a book. 92 - 93 - Specify a percentage value between 0 and 100 to indicate how far you've 94 - progressed through the book. Automatically updates status to 'reading' if not 95 - already set. 96 - 97 - **Usage:** 98 - 99 - ```bash 100 - noteleaf media book progress <id> <percentage> 101 - ``` 102 - 103 - #### update 104 - 105 - Change a book's status directly. 106 - 107 - Valid statuses are: queued (not started), reading (in progress), finished 108 - (completed), or removed (no longer tracking). 109 - 110 - **Usage:** 111 - 112 - ```bash 113 - noteleaf media book update <id> <status> 114 - ``` 115 -
-78
website/docs/manual/configuration.md
··· 1 - --- 2 - id: configuration 3 - title: Configuration 4 - sidebar_position: 7 5 - description: Manage application configuration 6 - --- 7 - 8 - ## config 9 - 10 - Manage noteleaf configuration 11 - 12 - ```bash 13 - noteleaf config 14 - ``` 15 - 16 - ### Subcommands 17 - 18 - #### get 19 - 20 - Display configuration values. 21 - 22 - If no key is provided, displays all configuration values. 23 - Otherwise, displays the value for the specified key. 24 - 25 - **Usage:** 26 - 27 - ```bash 28 - noteleaf config get [key] 29 - ``` 30 - 31 - #### set 32 - 33 - Update a configuration value. 34 - 35 - Available keys: 36 - database_path - Custom database file path 37 - data_dir - Custom data directory 38 - date_format - Date format string (default: 2006-01-02) 39 - color_scheme - Color scheme (default: default) 40 - default_view - Default view mode (default: list) 41 - default_priority - Default task priority 42 - editor - Preferred text editor 43 - articles_dir - Articles storage directory 44 - notes_dir - Notes storage directory 45 - auto_archive - Auto-archive completed items (true/false) 46 - sync_enabled - Enable synchronization (true/false) 47 - sync_endpoint - Synchronization endpoint URL 48 - sync_token - Synchronization token 49 - export_format - Default export format (default: json) 50 - movie_api_key - API key for movie database 51 - book_api_key - API key for book database 52 - 53 - **Usage:** 54 - 55 - ```bash 56 - noteleaf config set <key> <value> 57 - ``` 58 - 59 - #### path 60 - 61 - Display the path to the configuration file being used. 62 - 63 - **Usage:** 64 - 65 - ```bash 66 - noteleaf config path 67 - ``` 68 - 69 - #### reset 70 - 71 - Reset all configuration values to their defaults. 72 - 73 - **Usage:** 74 - 75 - ```bash 76 - noteleaf config reset 77 - ``` 78 -
-37
website/docs/manual/index.md
··· 1 - --- 2 - id: index 3 - title: CLI Reference 4 - sidebar_label: Overview 5 - sidebar_position: 0 6 - description: Complete command-line reference for noteleaf 7 - --- 8 - 9 - # noteleaf CLI Reference 10 - 11 - noteleaf - personal information manager for the command line 12 - 13 - A comprehensive CLI tool for managing tasks, notes, articles, and media queues. 14 - Inspired by TaskWarrior, noteleaf combines todo management with reading lists, 15 - watch queues, and a personal knowledge base. 16 - 17 - Core features include hierarchical tasks with dependencies, recurring tasks, 18 - time tracking, markdown notes with tags, article archiving, and media queue 19 - management for books, movies, and TV shows. 20 - 21 - ## Usage 22 - 23 - ```bash 24 - noteleaf 25 - ``` 26 - 27 - ## Command Groups 28 - 29 - - **[Task Management](tasks)** - Manage todos, projects, and time tracking 30 - - **[Notes](notes)** - Create and organize markdown notes 31 - - **[Articles](articles)** - Save and archive web articles 32 - - **[Books](books)** - Track reading list and progress 33 - - **[Movies](movies)** - Manage movie watch queue 34 - - **[TV Shows](tv-shows)** - Track TV show watching 35 - - **[Configuration](configuration)** - Manage settings 36 - - **[Management](management)** - Application management 37 -
-61
website/docs/manual/management.md
··· 1 - --- 2 - id: management 3 - title: Management 4 - sidebar_position: 8 5 - description: Application management commands 6 - --- 7 - 8 - ## status 9 - 10 - Display comprehensive application status information. 11 - 12 - Shows database location, configuration file path, data directories, and current 13 - settings. Use this command to verify your noteleaf installation and diagnose 14 - configuration issues. 15 - 16 - ```bash 17 - noteleaf status 18 - ``` 19 - 20 - ## setup 21 - 22 - Initialize noteleaf for first use. 23 - 24 - Creates the database, configuration file, and required data directories. Run 25 - this command after installing noteleaf or when setting up a new environment. 26 - Safe to run multiple times as it will skip existing resources. 27 - 28 - ```bash 29 - noteleaf setup 30 - ``` 31 - 32 - ### Subcommands 33 - 34 - #### seed 35 - 36 - Add sample tasks, books, and notes to the database for testing and demonstration purposes 37 - 38 - **Usage:** 39 - 40 - ```bash 41 - noteleaf setup seed [flags] 42 - ``` 43 - 44 - **Options:** 45 - 46 - ``` 47 - -f, --force Clear existing data and re-seed 48 - ``` 49 - 50 - ## reset 51 - 52 - Remove all application data and return to initial state. 53 - 54 - This command deletes the database, all media files, notes, and articles. The 55 - configuration file is preserved. Use with caution as this operation cannot be 56 - undone. You will be prompted for confirmation before deletion proceeds. 57 - 58 - ```bash 59 - noteleaf reset 60 - ``` 61 -
-77
website/docs/manual/movies.md
··· 1 - --- 2 - id: movies 3 - title: Movies 4 - sidebar_position: 5 5 - description: Track movies in watch queue 6 - --- 7 - 8 - ## movie 9 - 10 - Track movies you want to watch. 11 - 12 - Search TMDB for movies and add them to your queue. Mark movies as watched when 13 - completed. Maintains a history of your movie watching activity. 14 - 15 - ```bash 16 - noteleaf media movie 17 - ``` 18 - 19 - ### Subcommands 20 - 21 - #### add 22 - 23 - Search for movies and add them to your watch queue. 24 - 25 - By default, shows search results in a simple list format where you can select by number. 26 - Use the -i flag for an interactive interface with navigation keys. 27 - 28 - **Usage:** 29 - 30 - ```bash 31 - noteleaf media movie add [search query...] [flags] 32 - ``` 33 - 34 - **Options:** 35 - 36 - ``` 37 - -i, --interactive Use interactive interface for movie selection 38 - ``` 39 - 40 - #### list 41 - 42 - Display movies in your queue with optional status filters. 43 - 44 - Shows movie titles, release years, and current status. Filter by --all to show 45 - everything, --watched for completed movies, or --queued for unwatched items. 46 - Default shows queued movies only. 47 - 48 - **Usage:** 49 - 50 - ```bash 51 - noteleaf media movie list [--all|--watched|--queued] 52 - ``` 53 - 54 - #### watched 55 - 56 - Mark a movie as watched with current timestamp. Moves the movie from queued to watched status. 57 - 58 - **Usage:** 59 - 60 - ```bash 61 - noteleaf media movie watched [id] 62 - ``` 63 - 64 - **Aliases:** seen 65 - 66 - #### remove 67 - 68 - Remove a movie from your watch queue. Use this for movies you no longer want to track. 69 - 70 - **Usage:** 71 - 72 - ```bash 73 - noteleaf media movie remove [id] 74 - ``` 75 - 76 - **Aliases:** rm 77 -
-114
website/docs/manual/notes.md
··· 1 - --- 2 - id: notes 3 - title: Notes 4 - sidebar_position: 2 5 - description: Create and organize markdown notes 6 - --- 7 - 8 - ## note 9 - 10 - Create and organize markdown notes with tags. 11 - 12 - Write notes in markdown format, organize them with tags, browse them in an 13 - interactive TUI, and edit them in your preferred editor. Notes are stored as 14 - files on disk with metadata tracked in the database. 15 - 16 - ```bash 17 - noteleaf note 18 - ``` 19 - 20 - ### Subcommands 21 - 22 - #### create 23 - 24 - Create a new markdown note. 25 - 26 - Provide a title and optional content inline, or use --interactive to open an 27 - editor. Use --file to import content from an existing markdown file. Notes 28 - support tags for organization and full-text search. 29 - 30 - Examples: 31 - noteleaf note create "Meeting notes" "Discussed project timeline" 32 - noteleaf note create -i 33 - noteleaf note create --file ~/documents/draft.md 34 - 35 - **Usage:** 36 - 37 - ```bash 38 - noteleaf note create [title] [content...] [flags] 39 - ``` 40 - 41 - **Options:** 42 - 43 - ``` 44 - -e, --editor Prompt to open note in editor after creation 45 - -f, --file string Create note from markdown file 46 - -i, --interactive Open interactive editor 47 - ``` 48 - 49 - **Aliases:** new 50 - 51 - #### list 52 - 53 - Opens interactive TUI browser for navigating and viewing notes 54 - 55 - **Usage:** 56 - 57 - ```bash 58 - noteleaf note list [--archived] [--static] [--tags=tag1,tag2] [flags] 59 - ``` 60 - 61 - **Options:** 62 - 63 - ``` 64 - -a, --archived Show archived notes 65 - -s, --static Show static list instead of interactive TUI 66 - --tags string Filter by tags (comma-separated) 67 - ``` 68 - 69 - **Aliases:** ls 70 - 71 - #### read 72 - 73 - Display note content with formatted markdown rendering. 74 - 75 - Shows the note with syntax highlighting, proper formatting, and metadata. 76 - Useful for quick viewing without opening an editor. 77 - 78 - **Usage:** 79 - 80 - ```bash 81 - noteleaf note read [note-id] 82 - ``` 83 - 84 - **Aliases:** view 85 - 86 - #### edit 87 - 88 - Open note in your configured text editor. 89 - 90 - Uses the editor specified in your noteleaf configuration or the EDITOR 91 - environment variable. Changes are automatically saved when you close the 92 - editor. 93 - 94 - **Usage:** 95 - 96 - ```bash 97 - noteleaf note edit [note-id] 98 - ``` 99 - 100 - #### remove 101 - 102 - Delete a note permanently. 103 - 104 - Removes both the markdown file and database metadata. This operation cannot be 105 - undone. You will be prompted for confirmation before deletion. 106 - 107 - **Usage:** 108 - 109 - ```bash 110 - noteleaf note remove [note-id] 111 - ``` 112 - 113 - **Aliases:** rm, delete, del 114 -
-861
website/docs/manual/tasks.md
··· 1 - --- 2 - id: task-management 3 - title: Task Management 4 - sidebar_position: 1 5 - description: Manage tasks with TaskWarrior-inspired features 6 - --- 7 - 8 - ## todo 9 - 10 - Manage tasks with TaskWarrior-inspired features. 11 - 12 - Track todos with priorities, projects, contexts, and tags. Supports hierarchical 13 - tasks with parent/child relationships, task dependencies, recurring tasks, and 14 - time tracking. Tasks can be filtered by status, priority, project, or context. 15 - 16 - ```bash 17 - noteleaf todo 18 - ``` 19 - 20 - ### Subcommands 21 - 22 - #### add 23 - 24 - Create a new task with description and optional attributes. 25 - 26 - Tasks can be created with priority levels (low, medium, high, urgent), assigned 27 - to projects and contexts, tagged for organization, and configured with due dates 28 - and recurrence rules. Dependencies can be established to ensure tasks are 29 - completed in order. 30 - 31 - Examples: 32 - noteleaf todo add "Write documentation" --priority high --project docs 33 - noteleaf todo add "Weekly review" --recur "FREQ=WEEKLY" --due 2024-01-15 34 - 35 - **Usage:** 36 - 37 - ```bash 38 - noteleaf todo add [description] [flags] 39 - ``` 40 - 41 - **Options:** 42 - 43 - ``` 44 - -c, --context string Set task context 45 - --depends-on string Set task dependencies (comma-separated UUIDs) 46 - -d, --due string Set due date (YYYY-MM-DD) 47 - --parent string Set parent task UUID 48 - -p, --priority string Set task priority 49 - --project string Set task project 50 - --recur string Set recurrence rule (e.g., FREQ=DAILY) 51 - -t, --tags strings Add tags to task 52 - --until string Set recurrence end date (YYYY-MM-DD) 53 - ``` 54 - 55 - **Aliases:** create, new 56 - 57 - #### list 58 - 59 - List tasks with optional filtering and display modes. 60 - 61 - By default, shows tasks in an interactive TaskWarrior-like interface. 62 - Use --static to show a simple text list instead. 63 - Use --all to show all tasks, otherwise only pending tasks are shown. 64 - 65 - **Usage:** 66 - 67 - ```bash 68 - noteleaf todo list [flags] 69 - ``` 70 - 71 - **Options:** 72 - 73 - ``` 74 - -a, --all Show all tasks (default: pending only) 75 - --context string Filter by context 76 - -i, --interactive Force interactive mode (default) 77 - --priority string Filter by priority 78 - --project string Filter by project 79 - --static Use static text output instead of interactive 80 - --status string Filter by status 81 - ``` 82 - 83 - **Aliases:** ls 84 - 85 - #### view 86 - 87 - Display detailed information for a specific task. 88 - 89 - Shows all task attributes including description, status, priority, project, 90 - context, tags, due date, creation time, and modification history. Use --json 91 - for machine-readable output or --no-metadata to show only the description. 92 - 93 - **Usage:** 94 - 95 - ```bash 96 - noteleaf todo view [task-id] [flags] 97 - ``` 98 - 99 - **Options:** 100 - 101 - ``` 102 - --format string Output format (detailed, brief) (default "detailed") 103 - --json Output as JSON 104 - --no-metadata Hide creation/modification timestamps 105 - ``` 106 - 107 - #### update 108 - 109 - Modify attributes of an existing task. 110 - 111 - Update any task property including description, status, priority, project, 112 - context, due date, recurrence rule, or parent task. Add or remove tags and 113 - dependencies. Multiple attributes can be updated in a single command. 114 - 115 - Examples: 116 - noteleaf todo update 123 --priority urgent --due tomorrow 117 - noteleaf todo update 456 --add-tag urgent --project website 118 - 119 - **Usage:** 120 - 121 - ```bash 122 - noteleaf todo update [task-id] [flags] 123 - ``` 124 - 125 - **Options:** 126 - 127 - ``` 128 - --add-depends string Add task dependencies (comma-separated UUIDs) 129 - --add-tag strings Add tags to task 130 - -c, --context string Set task context 131 - --description string Update task description 132 - -d, --due string Set due date (YYYY-MM-DD) 133 - --parent string Set parent task UUID 134 - -p, --priority string Set task priority 135 - --project string Set task project 136 - --recur string Set recurrence rule (e.g., FREQ=DAILY) 137 - --remove-depends string Remove task dependencies (comma-separated UUIDs) 138 - --remove-tag strings Remove tags from task 139 - --status string Update task status 140 - -t, --tags strings Add tags to task 141 - --until string Set recurrence end date (YYYY-MM-DD) 142 - ``` 143 - 144 - #### edit 145 - 146 - Open interactive editor for task modification. 147 - 148 - Provides a user-friendly interface with status picker and priority toggle. 149 - Easier than using multiple command-line flags for complex updates. 150 - 151 - **Usage:** 152 - 153 - ```bash 154 - noteleaf todo edit [task-id] 155 - ``` 156 - 157 - **Aliases:** e 158 - 159 - #### delete 160 - 161 - Permanently remove a task from the database. 162 - 163 - This operation cannot be undone. Consider updating the task status to 164 - 'deleted' instead if you want to preserve the record for historical purposes. 165 - 166 - **Usage:** 167 - 168 - ```bash 169 - noteleaf todo delete [task-id] 170 - ``` 171 - 172 - #### projects 173 - 174 - Display all projects with task counts. 175 - 176 - Shows each project used in your tasks along with the number of tasks in each 177 - project. Use --todo-txt to format output with +project syntax for compatibility 178 - with todo.txt tools. 179 - 180 - **Usage:** 181 - 182 - ```bash 183 - noteleaf todo projects [flags] 184 - ``` 185 - 186 - **Options:** 187 - 188 - ``` 189 - --static Use static text output instead of interactive 190 - --todo-txt Format output with +project prefix for todo.txt compatibility 191 - ``` 192 - 193 - **Aliases:** proj 194 - 195 - #### tags 196 - 197 - Display all tags used across tasks. 198 - 199 - Shows each tag with the number of tasks using it. Tags provide flexible 200 - categorization orthogonal to projects and contexts. 201 - 202 - **Usage:** 203 - 204 - ```bash 205 - noteleaf todo tags [flags] 206 - ``` 207 - 208 - **Options:** 209 - 210 - ``` 211 - --static Use static text output instead of interactive 212 - ``` 213 - 214 - **Aliases:** t 215 - 216 - #### contexts 217 - 218 - Display all contexts with task counts. 219 - 220 - Contexts represent locations or environments where tasks can be completed (e.g., 221 - @home, @office, @errands). Use --todo-txt to format output with @context syntax 222 - for compatibility with todo.txt tools. 223 - 224 - **Usage:** 225 - 226 - ```bash 227 - noteleaf todo contexts [flags] 228 - ``` 229 - 230 - **Options:** 231 - 232 - ``` 233 - --static Use static text output instead of interactive 234 - --todo-txt Format output with @context prefix for todo.txt compatibility 235 - ``` 236 - 237 - **Aliases:** con, loc, ctx, locations 238 - 239 - #### done 240 - 241 - Mark a task as completed with current timestamp. 242 - 243 - Sets the task status to 'completed' and records the completion time. For 244 - recurring tasks, generates the next instance based on the recurrence rule. 245 - 246 - **Usage:** 247 - 248 - ```bash 249 - noteleaf todo done [task-id] 250 - ``` 251 - 252 - **Aliases:** complete 253 - 254 - #### start 255 - 256 - Begin tracking time spent on a task. 257 - 258 - Records the start time for a work session. Only one task can be actively 259 - tracked at a time. Use --note to add a description of what you're working on. 260 - 261 - **Usage:** 262 - 263 - ```bash 264 - noteleaf todo start [task-id] [flags] 265 - ``` 266 - 267 - **Options:** 268 - 269 - ``` 270 - -n, --note string Add a note to the time entry 271 - ``` 272 - 273 - #### stop 274 - 275 - End time tracking for the active task. 276 - 277 - Records the end time and calculates duration for the current work session. 278 - Duration is added to the task's total time tracked. 279 - 280 - **Usage:** 281 - 282 - ```bash 283 - noteleaf todo stop [task-id] 284 - ``` 285 - 286 - #### timesheet 287 - 288 - Show time tracking summary for tasks. 289 - 290 - By default shows time entries for the last 7 days. 291 - Use --task to show timesheet for a specific task. 292 - Use --days to change the date range. 293 - 294 - **Usage:** 295 - 296 - ```bash 297 - noteleaf todo timesheet [flags] 298 - ``` 299 - 300 - **Options:** 301 - 302 - ``` 303 - -d, --days int Number of days to show in timesheet (default 7) 304 - -t, --task string Show timesheet for specific task ID 305 - ``` 306 - 307 - #### recur 308 - 309 - Configure recurring task patterns. 310 - 311 - Create tasks that repeat on a schedule using iCalendar recurrence rules (RRULE). 312 - Supports daily, weekly, monthly, and yearly patterns with optional end dates. 313 - 314 - **Usage:** 315 - 316 - ```bash 317 - noteleaf todo recur 318 - ``` 319 - 320 - **Aliases:** repeat 321 - 322 - ##### set 323 - 324 - Apply a recurrence rule to create repeating task instances. 325 - 326 - Uses iCalendar RRULE syntax (e.g., "FREQ=DAILY" for daily tasks, "FREQ=WEEKLY;BYDAY=MO,WE,FR" 327 - for specific weekdays). When a recurring task is completed, the next instance is 328 - automatically generated. 329 - 330 - Examples: 331 - noteleaf todo recur set 123 --rule "FREQ=DAILY" 332 - noteleaf todo recur set 456 --rule "FREQ=WEEKLY;BYDAY=MO" --until 2024-12-31 333 - 334 - **Usage:** 335 - 336 - ```bash 337 - noteleaf todo recur set [task-id] [flags] 338 - ``` 339 - 340 - **Options:** 341 - 342 - ``` 343 - --rule string Recurrence rule (e.g., FREQ=DAILY) 344 - --until string Recurrence end date (YYYY-MM-DD) 345 - ``` 346 - 347 - ##### clear 348 - 349 - Remove recurrence from a task. 350 - 351 - Converts a recurring task to a one-time task. Existing future instances are not 352 - affected. 353 - 354 - **Usage:** 355 - 356 - ```bash 357 - noteleaf todo recur clear [task-id] 358 - ``` 359 - 360 - ##### show 361 - 362 - Display recurrence rule and schedule information. 363 - 364 - Shows the RRULE pattern, next occurrence date, and recurrence end date if 365 - configured. 366 - 367 - **Usage:** 368 - 369 - ```bash 370 - noteleaf todo recur show [task-id] 371 - ``` 372 - 373 - #### depend 374 - 375 - Create and manage task dependencies. 376 - 377 - Establish relationships where one task must be completed before another can 378 - begin. Useful for multi-step workflows and project management. 379 - 380 - **Usage:** 381 - 382 - ```bash 383 - noteleaf todo depend 384 - ``` 385 - 386 - **Aliases:** dep, deps 387 - 388 - ##### add 389 - 390 - Make a task dependent on another task's completion. 391 - 392 - The first task cannot be started until the second task is completed. Use task 393 - UUIDs to specify dependencies. 394 - 395 - **Usage:** 396 - 397 - ```bash 398 - noteleaf todo depend add [task-id] [depends-on-uuid] 399 - ``` 400 - 401 - ##### remove 402 - 403 - Delete a dependency relationship between two tasks. 404 - 405 - **Usage:** 406 - 407 - ```bash 408 - noteleaf todo depend remove [task-id] [depends-on-uuid] 409 - ``` 410 - 411 - **Aliases:** rm 412 - 413 - ##### list 414 - 415 - Show all tasks that must be completed before this task can be started. 416 - 417 - **Usage:** 418 - 419 - ```bash 420 - noteleaf todo depend list [task-id] 421 - ``` 422 - 423 - **Aliases:** ls 424 - 425 - ##### blocked-by 426 - 427 - Display all tasks that depend on this task's completion. 428 - 429 - **Usage:** 430 - 431 - ```bash 432 - noteleaf todo depend blocked-by [task-id] 433 - ``` 434 - 435 - ## todo 436 - 437 - Manage tasks with TaskWarrior-inspired features. 438 - 439 - Track todos with priorities, projects, contexts, and tags. Supports hierarchical 440 - tasks with parent/child relationships, task dependencies, recurring tasks, and 441 - time tracking. Tasks can be filtered by status, priority, project, or context. 442 - 443 - ```bash 444 - noteleaf todo 445 - ``` 446 - 447 - ### Subcommands 448 - 449 - #### add 450 - 451 - Create a new task with description and optional attributes. 452 - 453 - Tasks can be created with priority levels (low, medium, high, urgent), assigned 454 - to projects and contexts, tagged for organization, and configured with due dates 455 - and recurrence rules. Dependencies can be established to ensure tasks are 456 - completed in order. 457 - 458 - Examples: 459 - noteleaf todo add "Write documentation" --priority high --project docs 460 - noteleaf todo add "Weekly review" --recur "FREQ=WEEKLY" --due 2024-01-15 461 - 462 - **Usage:** 463 - 464 - ```bash 465 - noteleaf todo add [description] [flags] 466 - ``` 467 - 468 - **Options:** 469 - 470 - ``` 471 - -c, --context string Set task context 472 - --depends-on string Set task dependencies (comma-separated UUIDs) 473 - -d, --due string Set due date (YYYY-MM-DD) 474 - --parent string Set parent task UUID 475 - -p, --priority string Set task priority 476 - --project string Set task project 477 - --recur string Set recurrence rule (e.g., FREQ=DAILY) 478 - -t, --tags strings Add tags to task 479 - --until string Set recurrence end date (YYYY-MM-DD) 480 - ``` 481 - 482 - **Aliases:** create, new 483 - 484 - #### list 485 - 486 - List tasks with optional filtering and display modes. 487 - 488 - By default, shows tasks in an interactive TaskWarrior-like interface. 489 - Use --static to show a simple text list instead. 490 - Use --all to show all tasks, otherwise only pending tasks are shown. 491 - 492 - **Usage:** 493 - 494 - ```bash 495 - noteleaf todo list [flags] 496 - ``` 497 - 498 - **Options:** 499 - 500 - ``` 501 - -a, --all Show all tasks (default: pending only) 502 - --context string Filter by context 503 - -i, --interactive Force interactive mode (default) 504 - --priority string Filter by priority 505 - --project string Filter by project 506 - --static Use static text output instead of interactive 507 - --status string Filter by status 508 - ``` 509 - 510 - **Aliases:** ls 511 - 512 - #### view 513 - 514 - Display detailed information for a specific task. 515 - 516 - Shows all task attributes including description, status, priority, project, 517 - context, tags, due date, creation time, and modification history. Use --json 518 - for machine-readable output or --no-metadata to show only the description. 519 - 520 - **Usage:** 521 - 522 - ```bash 523 - noteleaf todo view [task-id] [flags] 524 - ``` 525 - 526 - **Options:** 527 - 528 - ``` 529 - --format string Output format (detailed, brief) (default "detailed") 530 - --json Output as JSON 531 - --no-metadata Hide creation/modification timestamps 532 - ``` 533 - 534 - #### update 535 - 536 - Modify attributes of an existing task. 537 - 538 - Update any task property including description, status, priority, project, 539 - context, due date, recurrence rule, or parent task. Add or remove tags and 540 - dependencies. Multiple attributes can be updated in a single command. 541 - 542 - Examples: 543 - noteleaf todo update 123 --priority urgent --due tomorrow 544 - noteleaf todo update 456 --add-tag urgent --project website 545 - 546 - **Usage:** 547 - 548 - ```bash 549 - noteleaf todo update [task-id] [flags] 550 - ``` 551 - 552 - **Options:** 553 - 554 - ``` 555 - --add-depends string Add task dependencies (comma-separated UUIDs) 556 - --add-tag strings Add tags to task 557 - -c, --context string Set task context 558 - --description string Update task description 559 - -d, --due string Set due date (YYYY-MM-DD) 560 - --parent string Set parent task UUID 561 - -p, --priority string Set task priority 562 - --project string Set task project 563 - --recur string Set recurrence rule (e.g., FREQ=DAILY) 564 - --remove-depends string Remove task dependencies (comma-separated UUIDs) 565 - --remove-tag strings Remove tags from task 566 - --status string Update task status 567 - -t, --tags strings Add tags to task 568 - --until string Set recurrence end date (YYYY-MM-DD) 569 - ``` 570 - 571 - #### edit 572 - 573 - Open interactive editor for task modification. 574 - 575 - Provides a user-friendly interface with status picker and priority toggle. 576 - Easier than using multiple command-line flags for complex updates. 577 - 578 - **Usage:** 579 - 580 - ```bash 581 - noteleaf todo edit [task-id] 582 - ``` 583 - 584 - **Aliases:** e 585 - 586 - #### delete 587 - 588 - Permanently remove a task from the database. 589 - 590 - This operation cannot be undone. Consider updating the task status to 591 - 'deleted' instead if you want to preserve the record for historical purposes. 592 - 593 - **Usage:** 594 - 595 - ```bash 596 - noteleaf todo delete [task-id] 597 - ``` 598 - 599 - #### projects 600 - 601 - Display all projects with task counts. 602 - 603 - Shows each project used in your tasks along with the number of tasks in each 604 - project. Use --todo-txt to format output with +project syntax for compatibility 605 - with todo.txt tools. 606 - 607 - **Usage:** 608 - 609 - ```bash 610 - noteleaf todo projects [flags] 611 - ``` 612 - 613 - **Options:** 614 - 615 - ``` 616 - --static Use static text output instead of interactive 617 - --todo-txt Format output with +project prefix for todo.txt compatibility 618 - ``` 619 - 620 - **Aliases:** proj 621 - 622 - #### tags 623 - 624 - Display all tags used across tasks. 625 - 626 - Shows each tag with the number of tasks using it. Tags provide flexible 627 - categorization orthogonal to projects and contexts. 628 - 629 - **Usage:** 630 - 631 - ```bash 632 - noteleaf todo tags [flags] 633 - ``` 634 - 635 - **Options:** 636 - 637 - ``` 638 - --static Use static text output instead of interactive 639 - ``` 640 - 641 - **Aliases:** t 642 - 643 - #### contexts 644 - 645 - Display all contexts with task counts. 646 - 647 - Contexts represent locations or environments where tasks can be completed (e.g., 648 - @home, @office, @errands). Use --todo-txt to format output with @context syntax 649 - for compatibility with todo.txt tools. 650 - 651 - **Usage:** 652 - 653 - ```bash 654 - noteleaf todo contexts [flags] 655 - ``` 656 - 657 - **Options:** 658 - 659 - ``` 660 - --static Use static text output instead of interactive 661 - --todo-txt Format output with @context prefix for todo.txt compatibility 662 - ``` 663 - 664 - **Aliases:** con, loc, ctx, locations 665 - 666 - #### done 667 - 668 - Mark a task as completed with current timestamp. 669 - 670 - Sets the task status to 'completed' and records the completion time. For 671 - recurring tasks, generates the next instance based on the recurrence rule. 672 - 673 - **Usage:** 674 - 675 - ```bash 676 - noteleaf todo done [task-id] 677 - ``` 678 - 679 - **Aliases:** complete 680 - 681 - #### start 682 - 683 - Begin tracking time spent on a task. 684 - 685 - Records the start time for a work session. Only one task can be actively 686 - tracked at a time. Use --note to add a description of what you're working on. 687 - 688 - **Usage:** 689 - 690 - ```bash 691 - noteleaf todo start [task-id] [flags] 692 - ``` 693 - 694 - **Options:** 695 - 696 - ``` 697 - -n, --note string Add a note to the time entry 698 - ``` 699 - 700 - #### stop 701 - 702 - End time tracking for the active task. 703 - 704 - Records the end time and calculates duration for the current work session. 705 - Duration is added to the task's total time tracked. 706 - 707 - **Usage:** 708 - 709 - ```bash 710 - noteleaf todo stop [task-id] 711 - ``` 712 - 713 - #### timesheet 714 - 715 - Show time tracking summary for tasks. 716 - 717 - By default shows time entries for the last 7 days. 718 - Use --task to show timesheet for a specific task. 719 - Use --days to change the date range. 720 - 721 - **Usage:** 722 - 723 - ```bash 724 - noteleaf todo timesheet [flags] 725 - ``` 726 - 727 - **Options:** 728 - 729 - ``` 730 - -d, --days int Number of days to show in timesheet (default 7) 731 - -t, --task string Show timesheet for specific task ID 732 - ``` 733 - 734 - #### recur 735 - 736 - Configure recurring task patterns. 737 - 738 - Create tasks that repeat on a schedule using iCalendar recurrence rules (RRULE). 739 - Supports daily, weekly, monthly, and yearly patterns with optional end dates. 740 - 741 - **Usage:** 742 - 743 - ```bash 744 - noteleaf todo recur 745 - ``` 746 - 747 - **Aliases:** repeat 748 - 749 - ##### set 750 - 751 - Apply a recurrence rule to create repeating task instances. 752 - 753 - Uses iCalendar RRULE syntax (e.g., "FREQ=DAILY" for daily tasks, "FREQ=WEEKLY;BYDAY=MO,WE,FR" 754 - for specific weekdays). When a recurring task is completed, the next instance is 755 - automatically generated. 756 - 757 - Examples: 758 - noteleaf todo recur set 123 --rule "FREQ=DAILY" 759 - noteleaf todo recur set 456 --rule "FREQ=WEEKLY;BYDAY=MO" --until 2024-12-31 760 - 761 - **Usage:** 762 - 763 - ```bash 764 - noteleaf todo recur set [task-id] [flags] 765 - ``` 766 - 767 - **Options:** 768 - 769 - ``` 770 - --rule string Recurrence rule (e.g., FREQ=DAILY) 771 - --until string Recurrence end date (YYYY-MM-DD) 772 - ``` 773 - 774 - ##### clear 775 - 776 - Remove recurrence from a task. 777 - 778 - Converts a recurring task to a one-time task. Existing future instances are not 779 - affected. 780 - 781 - **Usage:** 782 - 783 - ```bash 784 - noteleaf todo recur clear [task-id] 785 - ``` 786 - 787 - ##### show 788 - 789 - Display recurrence rule and schedule information. 790 - 791 - Shows the RRULE pattern, next occurrence date, and recurrence end date if 792 - configured. 793 - 794 - **Usage:** 795 - 796 - ```bash 797 - noteleaf todo recur show [task-id] 798 - ``` 799 - 800 - #### depend 801 - 802 - Create and manage task dependencies. 803 - 804 - Establish relationships where one task must be completed before another can 805 - begin. Useful for multi-step workflows and project management. 806 - 807 - **Usage:** 808 - 809 - ```bash 810 - noteleaf todo depend 811 - ``` 812 - 813 - **Aliases:** dep, deps 814 - 815 - ##### add 816 - 817 - Make a task dependent on another task's completion. 818 - 819 - The first task cannot be started until the second task is completed. Use task 820 - UUIDs to specify dependencies. 821 - 822 - **Usage:** 823 - 824 - ```bash 825 - noteleaf todo depend add [task-id] [depends-on-uuid] 826 - ``` 827 - 828 - ##### remove 829 - 830 - Delete a dependency relationship between two tasks. 831 - 832 - **Usage:** 833 - 834 - ```bash 835 - noteleaf todo depend remove [task-id] [depends-on-uuid] 836 - ``` 837 - 838 - **Aliases:** rm 839 - 840 - ##### list 841 - 842 - Show all tasks that must be completed before this task can be started. 843 - 844 - **Usage:** 845 - 846 - ```bash 847 - noteleaf todo depend list [task-id] 848 - ``` 849 - 850 - **Aliases:** ls 851 - 852 - ##### blocked-by 853 - 854 - Display all tasks that depend on this task's completion. 855 - 856 - **Usage:** 857 - 858 - ```bash 859 - noteleaf todo depend blocked-by [task-id] 860 - ``` 861 -
-91
website/docs/manual/tv-shows.md
··· 1 - --- 2 - id: tv-shows 3 - title: TV Shows 4 - sidebar_position: 6 5 - description: Manage TV show watching 6 - --- 7 - 8 - ## tv 9 - 10 - Track TV shows and episodes. 11 - 12 - Search TMDB for TV shows and add them to your queue. Track which shows you're 13 - currently watching, mark episodes as watched, and maintain a complete history 14 - of your viewing activity. 15 - 16 - ```bash 17 - noteleaf media tv 18 - ``` 19 - 20 - ### Subcommands 21 - 22 - #### add 23 - 24 - Search for TV shows and add them to your watch queue. 25 - 26 - By default, shows search results in a simple list format where you can select by number. 27 - Use the -i flag for an interactive interface with navigation keys. 28 - 29 - **Usage:** 30 - 31 - ```bash 32 - noteleaf media tv add [search query...] [flags] 33 - ``` 34 - 35 - **Options:** 36 - 37 - ``` 38 - -i, --interactive Use interactive interface for TV show selection 39 - ``` 40 - 41 - #### list 42 - 43 - Display TV shows in your queue with optional status filters. 44 - 45 - Shows show titles, air dates, and current status. Filter by --all, --queued, 46 - --watching for shows in progress, or --watched for completed series. Default 47 - shows queued shows only. 48 - 49 - **Usage:** 50 - 51 - ```bash 52 - noteleaf media tv list [--all|--queued|--watching|--watched] 53 - ``` 54 - 55 - #### watching 56 - 57 - Mark a TV show as currently watching. Use this when you start watching a series. 58 - 59 - **Usage:** 60 - 61 - ```bash 62 - noteleaf media tv watching [id] 63 - ``` 64 - 65 - #### watched 66 - 67 - Mark TV show episodes or entire series as watched. 68 - 69 - Updates episode tracking and completion status. Can mark individual episodes 70 - or complete seasons/series depending on ID format. 71 - 72 - **Usage:** 73 - 74 - ```bash 75 - noteleaf media tv watched [id] 76 - ``` 77 - 78 - **Aliases:** seen 79 - 80 - #### remove 81 - 82 - Remove a TV show from your watch queue. Use this for shows you no longer want to track. 83 - 84 - **Usage:** 85 - 86 - ```bash 87 - noteleaf media tv remove [id] 88 - ``` 89 - 90 - **Aliases:** rm 91 -
+9
website/docs/media/_category_.json
··· 1 + { 2 + "label": "Media", 3 + "position": 5, 4 + "link": { 5 + "type": "generated-index", 6 + "title": "Media Tracking", 7 + "description": "Track books, movies, and TV shows without leaving the terminal." 8 + } 9 + }
+100
website/docs/media/books.md
··· 1 + --- 2 + title: Books 3 + sidebar_label: Books 4 + description: Build and maintain your reading list with Open Library metadata. 5 + sidebar_position: 2 6 + --- 7 + 8 + # Books 9 + 10 + The book workflow revolves around Open Library search results. Each command lives under `noteleaf media book`. 11 + 12 + ## Add Books 13 + 14 + Search Open Library and pick a result: 15 + 16 + ```sh 17 + noteleaf media book add "Project Hail Mary" 18 + ``` 19 + 20 + Flags: 21 + 22 + - `-i, --interactive`: open the TUI browser (currently shows your local listโ€”useful for triage). 23 + - Plain mode prints the top five matches inline and prompts for a numeric selection. 24 + 25 + Behind the scenes Noteleaf records the title, authors, edition details, and any subjects returned by the API. New entries start in the `queued` status. 26 + 27 + ## Manage the Reading List 28 + 29 + List and filter: 30 + 31 + ```sh 32 + # Everything 33 + noteleaf media book list --all 34 + 35 + # Only active reads 36 + noteleaf media book list --reading 37 + 38 + # Completed books 39 + noteleaf media book list --finished 40 + ``` 41 + 42 + Each line shows the ID, title, author, status, progress percentage, and any captured metadata (publishers, edition counts, etc.). 43 + 44 + Remove items you no longer care about: 45 + 46 + ```sh 47 + noteleaf media book remove 42 48 + ``` 49 + 50 + ## Track Progress 51 + 52 + You can explicitly set the status: 53 + 54 + ```sh 55 + noteleaf media book reading 7 56 + noteleaf media book finished 7 57 + noteleaf media book update 7 queued 58 + ``` 59 + 60 + But the fastest way is to update the percentage: 61 + 62 + ```sh 63 + noteleaf media book progress 7 45 # Moves status to reading and records start time 64 + noteleaf media book progress 7 100 # Marks finished and records completion time 65 + ``` 66 + 67 + Logic applied automatically: 68 + 69 + - `0%` โ†’ resets to `queued` and clears the โ€œstartedโ€ timestamp. 70 + - `1โ€‘99%` โ†’ flips to `reading` (start time captured). 71 + - `100%` โ†’ marks `finished`, sets end time, and locks progress at 100%. 72 + 73 + ## Reading Lists and Search 74 + 75 + Common workflows: 76 + 77 + - **Focus view**: `noteleaf media book list --reading | fzf` to pick the next session book. 78 + - **Backlog grooming**: `noteleaf media book list --queued` to prune items before they go stale. 79 + - **Author sprint**: pipe the list to `rg` to filter by author (`noteleaf media book list --all | rg "Le Guin"`). 80 + 81 + The TUI (`noteleaf media book add -i` or `noteleaf media book list` with the `--interactive` switch) supports `/` to search titles/authors/notes live and `v` for a detailed preview with timestamps. 82 + 83 + ## Metadata and Notes 84 + 85 + Each record stores: 86 + 87 + - Title & authors (comma separated when multiple). 88 + - Edition count, publishers, subject tags, or cover IDs exposed as inline notes. 89 + - Added/started/finished timestamps. 90 + - Optional page count (if Open Library exposes it). 91 + 92 + Use those IDs anywhere else (tasks or notes). Example note snippet: 93 + 94 + ```markdown 95 + ## Reading Log 96 + - 2024-02-01 โ†’ Started book #7 ("Project Hail Mary") 97 + - 2024-02-05 โ†’ Captured ideas in note #128 linked back to the book. 98 + ``` 99 + 100 + Because media lives in the same database as tasks and notes, full-text search will surface those references instantly.
+75
website/docs/media/movies.md
··· 1 + --- 2 + title: Movies 3 + sidebar_label: Movies 4 + description: Keep track of your movie queue with Rotten Tomatoes metadata. 5 + sidebar_position: 3 6 + --- 7 + 8 + # Movies 9 + 10 + Movie commands hang off `noteleaf media movie`. Results use Rotten Tomatoes search so you get consistent titles plus critic scores. 11 + 12 + ## Add Movies 13 + 14 + ```sh 15 + noteleaf media movie add "The Matrix" 16 + ``` 17 + 18 + What happens: 19 + 20 + 1. The CLI fetches the first five Rotten Tomatoes matches. 21 + 2. You select the right one by number. 22 + 3. The chosen movie is inserted into the local queue with status `queued`. 23 + 24 + The `-i/--interactive` flag is reserved for a future selector; currently the inline prompt is the quickest path. 25 + 26 + ## List and Filter 27 + 28 + ```sh 29 + # Default: queued items only 30 + noteleaf media movie list 31 + 32 + # Include everything 33 + noteleaf media movie list --all 34 + 35 + # Review history 36 + noteleaf media movie list --watched 37 + ``` 38 + 39 + Each entry shows: 40 + 41 + - `ID` and title. 42 + - Release year (if Rotten Tomatoes provided one). 43 + - Status (`queued` or `watched`). 44 + - Critic score snippet (stored inside the Notes column). 45 + - Watched timestamp for completed items. 46 + 47 + ## Mark Movies as Watched 48 + 49 + ```sh 50 + noteleaf media movie watched 12 51 + ``` 52 + 53 + The command sets the status to `watched` and records `watched_at` using the current timestamp. Removing an item uses the same ID: 54 + 55 + ```sh 56 + noteleaf media movie remove 12 57 + ``` 58 + 59 + Use removal for titles you abandoned or added by mistakeโ€”the CLI deletes the database entry so your queue stays focused. 60 + 61 + ## Metadata Cheat Sheet 62 + 63 + - **Notes field**: includes critic score, whether Rotten Tomatoes marked it โ€œCertified Fresh,โ€ and the canonical URL. 64 + - **Rating column**: reserved for future personal ratings; right now it mirrors the upstream critic context. 65 + - **Timestamps**: `added` when you saved it, `watched` when you complete it. 66 + 67 + To keep a running diary, drop the IDs into a note: 68 + 69 + ```markdown 70 + ### Queue Ideas 71 + - Movie #31 โ†’ Watch before sequel comes out 72 + - Movie #12 โ†’ Pair with article #5 for cyberpunk research 73 + ``` 74 + 75 + This keeps everything searchable without having to leave the terminal.
+74
website/docs/media/organization.md
··· 1 + --- 2 + title: Media Organization 3 + sidebar_label: Organization 4 + description: Keep queues manageable with filters, reviews, and note links. 5 + sidebar_position: 5 6 + --- 7 + 8 + # Media Organization 9 + 10 + Media entries share the same database as tasks and notes, so you can cross-reference everything. This page outlines the practical workflows for keeping large queues in check. 11 + 12 + ## Tags and Categories 13 + 14 + Dedicated media tags have not shipped yet. Until they do: 15 + 16 + - Use the free-form `Notes` column (populated automatically from Open Library or Rotten Tomatoes) to stash keywords such as โ€œHugo shortlistโ€ or โ€œDocumentaryโ€. 17 + - When you need stricter structure, create a note that tracks an ad-hoc category and reference media IDs inside it: 18 + 19 + ```markdown 20 + ## Cozy backlog 21 + - Book #11 โ€“ comfort reread 22 + - Movie #25 โ€“ rainy-day pick 23 + ``` 24 + 25 + Full-text search (`noteleaf note list` โ†’ `/` and search) will surface the note instantly, and the numeric IDs jump you right back into the media commands. 26 + 27 + ## Custom Lists 28 + 29 + You already get status-based filters out of the box: 30 + 31 + ```sh 32 + noteleaf media book list --reading 33 + noteleaf media movie list --watched 34 + noteleaf media tv list --all | rg "FX" # filter with ripgrep 35 + ``` 36 + 37 + For more bespoke dashboards: 38 + 39 + 1. Use `noteleaf status` to grab the SQLite path. 40 + 2. Query it with tools like `sqlite-utils` or `datasette` to build spreadsheets or dashboards. 41 + 3. Export subsets via `sqlite3 noteleaf.db "SELECT * FROM books WHERE status='reading'" > reading.csv`. 42 + 43 + That approach keeps the CLI fast while still letting you slice the data any way you need. 44 + 45 + ## Ratings and Reviews 46 + 47 + The database schema already includes a `rating` column for every media type. Rotten Tomatoes/Open Library populate it with critic hints for now; personal star ratings will become editable in a future release. 48 + 49 + Until then, keep reviews as regular notes: 50 + 51 + ```sh 52 + noteleaf note create "Thoughts on Book #7" 53 + ``` 54 + 55 + Inside the note, link back to the record (`Book #7`, `Movie #18`, etc.) so searches tie everything together. Because notes live on disk you can also version-control your reviews. 56 + 57 + ## Linking Media to Notes 58 + 59 + There is no special โ€œlinkโ€ command yet, but the following pattern works well: 60 + 61 + 1. Create a dedicated note per book/movie/show (or per collection). 62 + 2. Add a heading with the media ID and paste the generated markdown path from `noteleaf article view` or the queue list. 63 + 3. Optionally embed checklists or quotes gathered while reading/watching. 64 + 65 + Example snippet: 66 + 67 + ```markdown 68 + ### Book #7 โ€” Project Hail Mary 69 + - Status: reading (45%) 70 + - Tasks: todo #128 covers the experiment described in chapter 12 71 + - Next action: finish Part II before Friday 72 + ``` 73 + 74 + Because tasks, notes, and media share the same SQLite file, future automation can join across them without migrations. When official linking lands it will reuse these IDs, so the prep work you do now keeps paying off.
+53
website/docs/media/overview.md
··· 1 + --- 2 + title: Media Overview 3 + sidebar_label: Overview 4 + description: Manage reading lists and watch queues from the CLI. 5 + sidebar_position: 1 6 + --- 7 + 8 + # Media Tracking Overview 9 + 10 + Noteleaf keeps book, movie, and TV data next to your tasks and notes so you do not need a separate โ€œwatch listโ€ app. 11 + All media commands hang off a single entry point: 12 + 13 + ```sh 14 + noteleaf media <book|movie|tv> <subcommand> 15 + ``` 16 + 17 + - **Books** pull metadata from the Open Library API. 18 + - **Movies/TV** scrape Rotten Tomatoes search results to capture critic scores and canonical titles. 19 + - Everything is stored in the local SQLite database located in your data directory (`~/.local/share/noteleaf` on Linux, `~/Library/Application Support/noteleaf` on macOS, `%LOCALAPPDATA%\noteleaf` on Windows). 20 + 21 + ## Lifecycle Statuses 22 + 23 + | Type | Statuses | Notes | 24 + | ------ | ------------------------------------------ | ------------------------------------------------------------------------------------------------- | 25 + | Books | `queued`, `reading`, `finished`, `removed` | Progress updates automatically bump status (0% โ†’ `queued`, 1-99% โ†’ `reading`, 100% โ†’ `finished`). | 26 + | Movies | `queued`, `watched`, `removed` | Marking as watched stores the completion timestamp. | 27 + | TV | `queued`, `watching`, `watched`, `removed` | Watching/watched commands also record the last watched time. | 28 + 29 + Statuses control list filtering and show up beside each item in the TUI. 30 + 31 + ## Metadata That Gets Saved 32 + 33 + - **Books**: title, authors, Open Library notes (editions, publishers, subjects), started/finished timestamps, progress percentage. 34 + - **Movies**: release year when available, Rotten Tomatoes critic score details inside the notes field, watched timestamp. 35 + - **TV**: show title plus critic score details, optional season/episode numbers, last watched timestamp. 36 + 37 + You can safely edit the generated markdown/notes in your favorite editorโ€”the records keep pointing to the updated files. 38 + 39 + ## Interactive vs Static Workflows 40 + 41 + All `list` commands default to a simple textual table. For books you can pass `-i/--interactive` to open the Bubble Tea list browser (TV and movie interactive selectors are planned). Inside the list view: 42 + 43 + - `j/k` or arrow keys move between entries. 44 + - `/` starts search across titles, authors, and metadata. 45 + - `v` opens a focused preview. 46 + - `?` shows all shortcuts. 47 + 48 + If you prefer scripts, combine the static lists with tools like `rg` or `jq`. 49 + 50 + ## Storage Layout 51 + 52 + Media records live in the SQLite database (`noteleaf.db`). Binary assets are not downloaded; the metadata stores canonical URLs so you can jump back to the source at any time. 53 + Use `noteleaf status` to see the exact paths for your database, data directory, and configuration file.
+64
website/docs/media/tv.md
··· 1 + --- 2 + title: TV Shows 3 + sidebar_label: TV Shows 4 + description: Track long-form series with simple queue management. 5 + sidebar_position: 4 6 + --- 7 + 8 + # TV Shows 9 + 10 + TV commands live under `noteleaf media tv`. Like movies, they use Rotten Tomatoes search so you can trust the spelling and canonical links. 11 + 12 + ## Add Shows 13 + 14 + ```sh 15 + noteleaf media tv add "Breaking Bad" 16 + ``` 17 + 18 + - Inline mode shows up to five matches and asks you to choose. 19 + - `-i/--interactive` is wired up for the future list selector. 20 + 21 + Every new show starts as `queued`. 22 + 23 + ## List the Queue 24 + 25 + ```sh 26 + noteleaf media tv list # queued shows 27 + noteleaf media tv list --watching # in-progress series 28 + noteleaf media tv list --watched # finished shows 29 + noteleaf media tv list --all # everything 30 + ``` 31 + 32 + Output includes the ID, title, optional season/episode numbers (once those fields are set), status, critic-score snippet, and timestamps. 33 + 34 + ## Update Status 35 + 36 + Use semantic verbs instead of editing the status manually: 37 + 38 + ```sh 39 + noteleaf media tv watching 8 # Moved to โ€œcurrently watchingโ€ 40 + noteleaf media tv watched 8 # Mark completed 41 + noteleaf media tv remove 8 # Drop from the queue entirely 42 + ``` 43 + 44 + Each transition records `last_watched` so you know when you left off. Future releases will expose explicit season/episode commands; until then store quick reminders in a linked note: 45 + 46 + ```markdown 47 + ### TV checklist 48 + - TV #8 โ€” resume Season 3 Episode 5 49 + - TV #15 โ€” waiting for new season announcement 50 + ``` 51 + 52 + ## Organization Tips 53 + 54 + - Use `noteleaf media tv list --watching | fzf` to pick tonightโ€™s episode. 55 + - Pipe `--all` into `rg "HBO"` to filter on the metadata snippet that contains the network/URL. 56 + - Include `TV #ID` references in your weekly review note so you can jump back with a single ID lookup. 57 + 58 + ## What Gets Stored 59 + 60 + - Rotten Tomatoes critic info plus canonical URL (inside the Notes column). 61 + - Optional season/episode integers for future episode tracking (already part of the schema). 62 + - Added timestamps and โ€œlast watchedโ€ timestamps. 63 + 64 + Because shows can last months, keeping the queue short (just what you plan to watch soon) makes the `list` output far easier to scan.
+9
website/docs/notes/_category_.json
··· 1 + { 2 + "label": "Notes", 3 + "position": 4, 4 + "link": { 5 + "type": "generated-index", 6 + "title": "Notes", 7 + "description": "Everything about Noteleaf's markdown knowledge base." 8 + } 9 + }
+61
website/docs/notes/advanced.md
··· 1 + --- 2 + title: Advanced Note Features 3 + sidebar_label: Advanced 4 + description: Search, exports, backlinks, and automation tips. 5 + sidebar_position: 5 6 + --- 7 + 8 + # Advanced Note Features 9 + 10 + ## Full-Text Search 11 + 12 + While not exposed as a dedicated command, you can search note content using the database: 13 + 14 + **Search with grep** (searches file content): 15 + 16 + ```sh 17 + grep -r "search term" ~/.local/share/noteleaf/notes/ 18 + ``` 19 + 20 + **Search titles and metadata**: 21 + 22 + ```sh 23 + noteleaf note list --static | grep "keyword" 24 + ``` 25 + 26 + Future versions may include built-in full-text search with relevance ranking. 27 + 28 + ## Note Exports 29 + 30 + Export notes to different formats using standard markdown tools: 31 + 32 + **Convert to HTML with pandoc**: 33 + 34 + ```sh 35 + noteleaf note view 1 --format=raw | pandoc -o output.html 36 + ``` 37 + 38 + **Convert to PDF**: 39 + 40 + ```sh 41 + noteleaf note view 1 --format=raw | pandoc -o output.pdf 42 + ``` 43 + 44 + **Batch export all notes**: 45 + 46 + ```sh 47 + for note in ~/.local/share/noteleaf/notes/*.md; do 48 + pandoc "$note" -o "${note%.md}.html" 49 + done 50 + ``` 51 + 52 + ## Backlinks and References 53 + 54 + Manually create backlinks between notes using markdown links: 55 + 56 + ```markdown 57 + See also: [[Research on Authentication]] for background 58 + Related: [[API Design Principles]] 59 + ``` 60 + 61 + While Noteleaf doesn't automatically parse or display backlinks yet, this syntax prepares notes for future backlink support and works with tools like Obsidian if you point it at the notes directory.
+57
website/docs/notes/basics.md
··· 1 + --- 2 + title: Note Basics 3 + sidebar_label: Basics 4 + description: Creating notes, metadata, and storage model. 5 + sidebar_position: 2 6 + --- 7 + 8 + # Note Basics 9 + 10 + ## Creation 11 + 12 + **Quick note from command line**: 13 + 14 + ```sh 15 + noteleaf note create "Meeting Notes" "Discussed Q4 roadmap and hiring plans" 16 + ``` 17 + 18 + **Interactive creation** (opens editor): 19 + 20 + ```sh 21 + noteleaf note create --interactive 22 + ``` 23 + 24 + **From existing file**: 25 + 26 + ```sh 27 + noteleaf note create --file ~/Documents/draft.md 28 + ``` 29 + 30 + **Create and immediately edit**: 31 + 32 + ```sh 33 + noteleaf note create "Research Notes" --editor 34 + ``` 35 + 36 + ## Structure 37 + 38 + Notes consist of: 39 + 40 + **Title**: Short descriptor shown in lists and searches. Can be updated later. 41 + 42 + **Content**: Full markdown text. Supports all standard markdown features including code blocks, lists, tables, and links. 43 + 44 + **Tags**: Categorization labels for organizing and filtering notes. Multiple tags per note. 45 + 46 + **Dates**: Creation and modification timestamps tracked automatically. 47 + 48 + **File Path**: Location of the markdown file on disk, managed by Noteleaf. 49 + 50 + ## Storage 51 + 52 + **File Location**: Notes are stored as individual `.md` files in your notes directory (typically `~/.local/share/noteleaf/notes` or `~/Library/Application Support/noteleaf/notes`). 53 + 54 + **Naming**: Files are named with a UUID to ensure uniqueness. The title is stored in the database, not the filename. 55 + 56 + **Portability**: Since notes are plain markdown, you can read them with any text editor or markdown viewer. 57 + The database provides additional functionality like tagging and search, but the files remain standalone.
+28
website/docs/notes/best-practices.md
··· 1 + --- 2 + title: Note Tips and Best Practices 3 + sidebar_label: Best Practices 4 + description: Guidelines for keeping notes useful over time. 5 + sidebar_position: 8 6 + --- 7 + 8 + # Note Tips and Best Practices 9 + 10 + **Write in plain language**: Notes are for your future self. Avoid jargon you might forget. 11 + 12 + **Tag consistently**: Establish a tagging taxonomy early and stick to it. Review tags periodically with `noteleaf note tags`. 13 + 14 + **Link liberally**: Reference related notes, tasks, and articles using markdown links and references. 15 + 16 + **Short, focused notes**: Better to have many small notes than few giant ones. Easier to link and reuse. 17 + 18 + **Regular review**: Schedule time to review notes, add tags, create links, and archive outdated content. 19 + 20 + **Edit immediately**: If you notice incomplete or unclear notes, edit them now. Stale notes lose value. 21 + 22 + **Use your editor**: Configure your favorite editor with markdown plugins for better syntax highlighting and live preview. 23 + 24 + **Backup regularly**: While notes are files, backup both the notes directory and the database to preserve metadata. 25 + 26 + **Experiment with formats**: Try different note structures (journal, zettelkasten, topic-based) to find what works for you. 27 + 28 + For CLI command reference, run `noteleaf note --help` or open the contextual help inside the TUI.
+103
website/docs/notes/markdown.md
··· 1 + --- 2 + title: Markdown Support 3 + sidebar_label: Markdown 4 + description: Reference for GitHub-Flavored Markdown features. 5 + sidebar_position: 6 6 + --- 7 + 8 + # Markdown Support 9 + 10 + Noteleaf notes support full GitHub-Flavored Markdown: 11 + 12 + ## Headers 13 + 14 + ```markdown 15 + # Level 1 16 + ## Level 2 17 + ### Level 3 18 + ``` 19 + 20 + ## Text Formatting 21 + 22 + ```markdown 23 + **bold** 24 + *italic* 25 + ***bold and italic*** 26 + ~~strikethrough~~ 27 + `inline code` 28 + ``` 29 + 30 + ## Lists 31 + 32 + ```markdown 33 + - Unordered item 34 + - Another item 35 + - Nested item 36 + 37 + 1. Ordered item 38 + 2. Second item 39 + 1. Nested ordered 40 + ``` 41 + 42 + ## Task Lists 43 + 44 + ```markdown 45 + - [x] Completed task 46 + - [ ] Pending task 47 + - [ ] Another pending 48 + ``` 49 + 50 + ## Links 51 + 52 + ```markdown 53 + [Link text](https://example.com) 54 + [Reference link][ref] 55 + 56 + [ref]: https://example.com 57 + ``` 58 + 59 + ## Images 60 + 61 + ```markdown 62 + ![Alt text](path/to/image.png) 63 + ![Remote image](https://example.com/image.png) 64 + ``` 65 + 66 + ## Code Blocks 67 + 68 + ````markdown 69 + ```python 70 + def hello(): 71 + print("Hello, world!") 72 + ``` 73 + 74 + ```javascript 75 + const greet = () => console.log("Hello!"); 76 + ``` 77 + ```` 78 + 79 + ## Tables 80 + 81 + ```markdown 82 + | Header 1 | Header 2 | 83 + |----------|----------| 84 + | Cell 1 | Cell 2 | 85 + | Cell 3 | Cell 4 | 86 + ``` 87 + 88 + ## Blockquotes 89 + 90 + ```markdown 91 + > This is a quote 92 + > spanning multiple lines 93 + > 94 + > With multiple paragraphs 95 + ``` 96 + 97 + ## Horizontal Rules 98 + 99 + ```markdown 100 + --- 101 + *** 102 + ___ 103 + ```
+73
website/docs/notes/operations.md
··· 1 + --- 2 + title: Note Operations 3 + sidebar_label: Operations 4 + description: List, read, edit, and delete notes from the CLI. 5 + sidebar_position: 3 6 + --- 7 + 8 + # Note Operations 9 + 10 + ### Listing Notes 11 + 12 + **Interactive TUI** (default): 13 + ```sh 14 + noteleaf note list 15 + ``` 16 + 17 + Navigate with arrow keys, press Enter to read, `e` to edit, `q` to quit. 18 + 19 + **Static list**: 20 + ```sh 21 + noteleaf note list --static 22 + ``` 23 + 24 + **Filter by tags**: 25 + ```sh 26 + noteleaf note list --tags research,technical 27 + ``` 28 + 29 + **Show archived notes**: 30 + ```sh 31 + noteleaf note list --archived 32 + ``` 33 + 34 + ### Reading Notes 35 + 36 + View note content with formatted rendering: 37 + 38 + ```sh 39 + noteleaf note read 1 40 + ``` 41 + 42 + Aliases: `noteleaf note view 1` 43 + 44 + The viewer renders markdown with syntax highlighting for code blocks, proper formatting for headers and lists, and displays metadata (title, tags, dates). 45 + 46 + ### Editing Notes 47 + 48 + Open note in your configured editor: 49 + 50 + ```sh 51 + noteleaf note edit 1 52 + ``` 53 + 54 + Noteleaf uses the editor specified in your configuration or the `$EDITOR` environment variable. Common choices: `vim`, `nvim`, `nano`, `code`, `emacs`. 55 + 56 + **Configure editor**: 57 + ```sh 58 + noteleaf config set editor nvim 59 + ``` 60 + 61 + Changes are saved automatically when you close the editor. The modification timestamp updates to track when notes were last changed. 62 + 63 + ### Deleting Notes 64 + 65 + Remove a note permanently: 66 + 67 + ```sh 68 + noteleaf note remove 1 69 + ``` 70 + 71 + Aliases: `rm`, `delete`, `del` 72 + 73 + This deletes both the markdown file and database metadata. You'll be prompted for confirmation. This operation cannot be undone, so consider archiving instead if you might need the note later.
+116
website/docs/notes/organization.md
··· 1 + --- 2 + title: Note Organization 3 + sidebar_label: Organization 4 + description: Tagging, linking, and template workflows for notes. 5 + sidebar_position: 4 6 + --- 7 + 8 + # Note Organization 9 + 10 + ## Tagging 11 + 12 + Tags provide flexible categorization without hierarchical constraints. 13 + 14 + **Add tags during creation**: 15 + 16 + ```sh 17 + noteleaf note create "API Design" --tags architecture,reference 18 + ``` 19 + 20 + **Add tags to existing note**: 21 + 22 + ```sh 23 + noteleaf note update 1 --add-tag reference 24 + ``` 25 + 26 + **Remove tags**: 27 + 28 + ```sh 29 + noteleaf note update 1 --remove-tag draft 30 + ``` 31 + 32 + **List all tags**: 33 + 34 + ```sh 35 + noteleaf note tags 36 + ``` 37 + 38 + Shows each tag with the count of notes using it. 39 + 40 + **Tag naming conventions**: Use lowercase, hyphens for compound tags. Examples: `research`, `meeting-notes`, `how-to`, `reference`, `technical`, `personal`. 41 + 42 + ## Linking 43 + 44 + While not a first-class feature in the current UI, notes can reference tasks by ID or description: 45 + 46 + ```markdown 47 + # Implementation Plan 48 + 49 + Related task: #42 (Deploy authentication service) 50 + 51 + ## Next Steps 52 + - Complete testing (task #43) 53 + - Write documentation (task #44) 54 + ``` 55 + 56 + Future versions may support automatic linking between notes and tasks in the database. 57 + 58 + ## Templates 59 + 60 + Create reusable note structures using shell functions or scripts: 61 + 62 + **In ~/.bashrc or ~/.zshrc**: 63 + 64 + ```sh 65 + meeting_note() { 66 + local title="Meeting: $1" 67 + local date=$(date +%Y-%m-%d) 68 + local content="# $title 69 + 70 + **Date**: $date 71 + 72 + ## Attendees 73 + - 74 + 75 + ## Agenda 76 + - 77 + 78 + ## Discussion 79 + - 80 + 81 + ## Action Items 82 + - [ ] 83 + 84 + ## Next Meeting 85 + - " 86 + 87 + echo "$content" | noteleaf note create "$title" --tags meeting --editor 88 + } 89 + 90 + daily_note() { 91 + local date=$(date +%Y-%m-%d) 92 + local title="Daily: $date" 93 + local content="# $title 94 + 95 + ## Completed Today 96 + - 97 + 98 + ## In Progress 99 + - 100 + 101 + ## Tomorrow's Focus 102 + - 103 + 104 + ## Notes 105 + - " 106 + 107 + echo "$content" | noteleaf note create "$title" --tags daily --editor 108 + } 109 + ``` 110 + 111 + Usage: 112 + 113 + ```sh 114 + meeting_note "Q4 Planning" 115 + daily_note 116 + ```
+11
website/docs/notes/overview.md
··· 1 + --- 2 + title: Notes Overview 3 + sidebar_label: Overview 4 + description: How Noteleaf manages markdown-based notes. 5 + sidebar_position: 1 6 + --- 7 + 8 + # Notes Overview 9 + 10 + Noteleaf provides a markdown-based note-taking system integrated with your tasks, articles, and media tracking. 11 + Notes are stored as individual markdown files with metadata tracked in the database, giving you both structure and portability.
+85
website/docs/notes/workflows.md
··· 1 + --- 2 + title: Note-Taking Workflows 3 + sidebar_label: Workflows 4 + description: Zettelkasten, meeting notes, daily notes, and more. 5 + sidebar_position: 7 6 + --- 7 + 8 + # Note-Taking Workflows 9 + 10 + ## Zettelkasten 11 + 12 + Zettelkasten emphasizes atomic notes with heavy linking: 13 + 14 + 1. **Create atomic notes**: Each note covers one concept 15 + 2. **Add descriptive tags**: Use tags for categorization 16 + 3. **Link related notes**: Reference other notes by title 17 + 4. **Develop ideas over time**: Expand notes with new insights 18 + 19 + Example: 20 + 21 + ```sh 22 + noteleaf note create "Dependency Injection" --tags architecture,patterns 23 + noteleaf note create "Inversion of Control" --tags architecture,patterns 24 + # In each note, reference the other 25 + ``` 26 + 27 + ### Research 28 + 29 + For academic or technical research: 30 + 31 + 1. **Source note per paper/article**: Create note for each source 32 + 2. **Extract key points**: Summarize in your own words 33 + 3. **Tag by topic**: Use consistent tags across research area 34 + 4. **Link to related work**: Reference other sources 35 + 36 + Example: 37 + 38 + ```sh 39 + noteleaf note create "Paper: Microservices Patterns" \ 40 + --tags research,architecture,microservices 41 + ``` 42 + 43 + ### Meeting 44 + 45 + Capture discussions and action items: 46 + 47 + 1. **Template-based**: Use meeting_note function from earlier 48 + 2. **Consistent structure**: Attendees, agenda, discussion, actions 49 + 3. **Action items**: Extract as tasks for follow-up 50 + 4. **Link to projects**: Tag with project name 51 + 52 + Example: 53 + 54 + ```sh 55 + meeting_note "Sprint Planning" 56 + # Then extract action items as tasks 57 + noteleaf task add "Implement auth endpoint" --project web-service 58 + ``` 59 + 60 + ### Daily 61 + 62 + Journal-style daily entries: 63 + 64 + 1. **Daily template**: Use daily_note function 65 + 2. **Reflect on work**: What was accomplished, what's next 66 + 3. **Capture ideas**: Random thoughts for later processing 67 + 4. **Review weekly**: Scan week's notes for patterns 68 + 69 + Example: 70 + 71 + ```sh 72 + daily_note 73 + # Creates note tagged with 'daily' and today's date 74 + ``` 75 + 76 + ### Personal Knowledge Base 77 + 78 + Build a reference library: 79 + 80 + 1. **How-to guides**: Document procedures and commands 81 + 2. **Troubleshooting notes**: Solutions to problems encountered 82 + 3. **Concept explanations**: Notes on topics you're learning 83 + 4. **Snippets**: Code examples and configurations 84 + 85 + Use tags like: `how-to`, `troubleshooting`, `reference`, `snippet`
+9
website/docs/tasks/_category_.json
··· 1 + { 2 + "label": "Tasks", 3 + "position": 3, 4 + "link": { 5 + "type": "generated-index", 6 + "title": "Task Management", 7 + "description": "Deep dives into scheduling, tracking, and organizing work." 8 + } 9 + }
+106
website/docs/tasks/advanced.md
··· 1 + --- 2 + title: Advanced Task Features 3 + sidebar_label: Advanced 4 + description: Recurring tasks, dependencies, hierarchies, and custom fields. 5 + sidebar_position: 6 6 + --- 7 + 8 + # Advanced Task Features 9 + 10 + ## Recurrence 11 + 12 + Create tasks that repeat on a schedule using iCalendar recurrence rules. 13 + 14 + **Daily task**: 15 + 16 + ```sh 17 + noteleaf task add "Daily standup" --recur "FREQ=DAILY" 18 + ``` 19 + 20 + **Weekly task on specific days**: 21 + 22 + ```sh 23 + noteleaf task add "Team meeting" --recur "FREQ=WEEKLY;BYDAY=MO,WE" 24 + ``` 25 + 26 + **Monthly task**: 27 + 28 + ```sh 29 + noteleaf task add "Invoice review" --recur "FREQ=MONTHLY;BYMONTHDAY=1" 30 + ``` 31 + 32 + **With end date**: 33 + 34 + ```sh 35 + noteleaf task add "Q1 review" \ 36 + --recur "FREQ=WEEKLY" \ 37 + --until 2025-03-31 38 + ``` 39 + 40 + **Manage recurrence**: 41 + 42 + Set recurrence on existing task: 43 + 44 + ```sh 45 + noteleaf task recur set 1 --rule "FREQ=DAILY" 46 + ``` 47 + 48 + View recurrence info: 49 + 50 + ```sh 51 + noteleaf task recur show 1 52 + ``` 53 + 54 + Remove recurrence: 55 + 56 + ```sh 57 + noteleaf task recur clear 1 58 + ``` 59 + 60 + When you complete a recurring task, Noteleaf automatically generates the next instance based on the recurrence rule. 61 + 62 + ## Dependencies 63 + 64 + Create relationships where tasks must be completed in order. 65 + 66 + **Add dependency** (task 1 depends on task 2): 67 + 68 + ```sh 69 + noteleaf task depend add 1 <uuid-of-task-2> 70 + ``` 71 + 72 + **List dependencies** (what must be done first): 73 + 74 + ```sh 75 + noteleaf task depend list 1 76 + ``` 77 + 78 + **List blocked tasks** (what's waiting on this task): 79 + 80 + ```sh 81 + noteleaf task depend blocked-by 1 82 + ``` 83 + 84 + **Remove dependency**: 85 + 86 + ```sh 87 + noteleaf task depend remove 1 <uuid-of-task-2> 88 + ``` 89 + 90 + Dependencies use task UUIDs (shown in `task view`) rather than IDs for stability across database changes. 91 + 92 + ## Hierarchical Tasks 93 + 94 + Create parent-child relationships for breaking down large tasks. 95 + 96 + **Create child task**: 97 + 98 + ```sh 99 + noteleaf task add "Write API documentation" --parent <parent-uuid> 100 + ``` 101 + 102 + Parent tasks can have multiple children, creating a tree structure for complex projects. 103 + 104 + ## Custom Attributes 105 + 106 + While not exposed through specific flags, the database schema supports extending tasks with custom attributes for advanced use cases or scripting.
+85
website/docs/tasks/basics.md
··· 1 + --- 2 + title: Task Basics 3 + sidebar_label: Basics 4 + description: Create tasks and understand their core attributes. 5 + sidebar_position: 2 6 + --- 7 + 8 + # Task Basics 9 + 10 + ## Creation 11 + 12 + Create a simple task: 13 + 14 + ```sh 15 + noteleaf task add "Write documentation" 16 + ``` 17 + 18 + Create a task with attributes: 19 + 20 + ```sh 21 + noteleaf task add "Review pull requests" \ 22 + --priority high \ 23 + --project work \ 24 + --tags urgent,code-review \ 25 + --due 2025-01-15 26 + ``` 27 + 28 + ## Properties 29 + 30 + **Description**: What needs to be done. Can be updated later with `task update`. 31 + 32 + **Status**: Task lifecycle state: 33 + 34 + - `pending`: Not yet started (default for new tasks) 35 + - `active`: Currently being worked on 36 + - `completed`: Finished successfully 37 + - `deleted`: Removed but preserved for history 38 + - `waiting`: Blocked or postponed 39 + 40 + **Priority**: Importance level affects sorting and display: 41 + 42 + - `low`: Nice to have, defer if busy 43 + - `medium`: Standard priority (default) 44 + - `high`: Important, should be done soon 45 + - `urgent`: Critical, top of the list 46 + 47 + **Project**: Group related tasks together. Examples: `work`, `home`, `side-project`. Projects create organizational boundaries and enable filtering. 48 + 49 + **Context**: Location or mode where task can be done. Examples: `@home`, `@office`, `@phone`, `@computer`. Contexts help filter tasks based on current situation. 50 + 51 + **Tags**: Flexible categorization orthogonal to projects. Examples: `urgent`, `quick-win`, `research`, `bug`. Multiple tags per task. 52 + 53 + **Due Date**: When the task should be completed. Format: `YYYY-MM-DD` or relative (`tomorrow`, `next week`). 54 + 55 + ### Lifecycle 56 + 57 + Tasks move through statuses as work progresses: 58 + 59 + ``` 60 + pending -> active -> completed 61 + | 62 + v 63 + waiting 64 + | 65 + v 66 + deleted 67 + ``` 68 + 69 + **Mark task as active**: 70 + 71 + ```sh 72 + noteleaf task update 1 --status active 73 + ``` 74 + 75 + **Complete a task**: 76 + 77 + ```sh 78 + noteleaf task done 1 79 + ``` 80 + 81 + **Delete a task**: 82 + 83 + ```sh 84 + noteleaf task delete 1 85 + ```
+26
website/docs/tasks/batch-operations.md
··· 1 + --- 2 + title: Batch Operations 3 + sidebar_label: Batch Ops 4 + description: Use shell scripting patterns for mass task edits. 5 + sidebar_position: 8 6 + --- 7 + 8 + # Batch Operations 9 + 10 + While Noteleaf doesn't have built-in bulk update commands, you can use shell scripting for batch operations: 11 + 12 + **Complete all tasks in a project**: 13 + 14 + ```sh 15 + noteleaf task list --project old-project --static | \ 16 + awk '{print $1}' | \ 17 + xargs -I {} noteleaf task done {} 18 + ``` 19 + 20 + **Add tag to multiple tasks**: 21 + 22 + ```sh 23 + for id in 1 2 3 4 5; do 24 + noteleaf task update $id --add-tag urgent 25 + done 26 + ```
+26
website/docs/tasks/best-practices.md
··· 1 + --- 2 + title: Task Tips and Best Practices 3 + sidebar_label: Best Practices 4 + description: Practical guidance for staying productive with tasks. 5 + sidebar_position: 10 6 + --- 7 + 8 + # Task Tips and Best Practices 9 + 10 + **Start with simple workflows**: Don't over-organize initially. Use basic priorities and projects before adding contexts, tags, and dependencies. 11 + 12 + **Review regularly**: Use `task list` daily to check pending work. Weekly reviews help catch stale tasks. 13 + 14 + **Use contexts for GTD**: If following Getting Things Done, contexts help filter tasks by what you can do right now. 15 + 16 + **Project-based work**: For complex initiatives, use projects to group tasks and dependencies to order work. 17 + 18 + **Time tracking for accountability**: Even rough time tracking reveals where hours go and helps with estimates. 19 + 20 + **Recurrence for habits**: Use recurring tasks for daily/weekly habits, but keep the list short to avoid clutter. 21 + 22 + **Tags for cross-cutting concerns**: Use tags for themes that span projects: `urgent`, `blocked`, `waiting-on-feedback`, `quick-win`. 23 + 24 + **JSON output for scripts**: Use `--json` flag with scripting to build custom reports and integrations. 25 + 26 + For CLI command reference, run `noteleaf task --help` or explore the inline help on each subcommand.
+114
website/docs/tasks/operations.md
··· 1 + --- 2 + title: Task Operations 3 + sidebar_label: Operations 4 + description: List, view, and update tasks from the CLI and TUI. 5 + sidebar_position: 3 6 + --- 7 + 8 + # Task Operations 9 + 10 + ## Listing and Filtering 11 + 12 + **Interactive list** (default): 13 + 14 + ```sh 15 + noteleaf task list 16 + ``` 17 + 18 + Navigate with arrow keys, press Enter to view details, `q` to quit. 19 + 20 + **Static list** (for scripting): 21 + 22 + ```sh 23 + noteleaf task list --static 24 + ``` 25 + 26 + **Filter by status**: 27 + 28 + ```sh 29 + noteleaf task list --status pending 30 + noteleaf task list --status completed 31 + ``` 32 + 33 + **Filter by project**: 34 + 35 + ```sh 36 + noteleaf task list --project work 37 + ``` 38 + 39 + **Filter by priority**: 40 + 41 + ```sh 42 + noteleaf task list --priority high 43 + ``` 44 + 45 + **Filter by context**: 46 + 47 + ```sh 48 + noteleaf task list --context @office 49 + ``` 50 + 51 + **Show all tasks** (including completed): 52 + 53 + ```sh 54 + noteleaf task list --all 55 + ``` 56 + 57 + ## Viewing Task Details 58 + 59 + View complete task information: 60 + 61 + ```sh 62 + noteleaf task view 1 63 + ``` 64 + 65 + JSON output for scripts: 66 + 67 + ```sh 68 + noteleaf task view 1 --json 69 + ``` 70 + 71 + Brief format without metadata: 72 + 73 + ```sh 74 + noteleaf task view 1 --format brief 75 + ``` 76 + 77 + ## Updating Tasks 78 + 79 + Update single attribute: 80 + 81 + ```sh 82 + noteleaf task update 1 --priority urgent 83 + ``` 84 + 85 + Update multiple attributes: 86 + 87 + ```sh 88 + noteleaf task update 1 \ 89 + --priority urgent \ 90 + --due tomorrow \ 91 + --add-tag critical 92 + ``` 93 + 94 + Change description: 95 + 96 + ```sh 97 + noteleaf task update 1 --description "New task description" 98 + ``` 99 + 100 + Add and remove tags: 101 + 102 + ```sh 103 + noteleaf task update 1 --add-tag urgent --remove-tag later 104 + ``` 105 + 106 + ## Interactive Editing 107 + 108 + Open interactive editor for complex changes: 109 + 110 + ```sh 111 + noteleaf task edit 1 112 + ``` 113 + 114 + This provides a TUI with visual pickers for status and priority, making updates faster than command flags.
+64
website/docs/tasks/organization.md
··· 1 + --- 2 + title: Task Organization 3 + sidebar_label: Organization 4 + description: Use projects, contexts, and tags to structure work. 5 + sidebar_position: 5 6 + --- 7 + 8 + # Task Organization 9 + 10 + ## Projects 11 + 12 + Projects group related tasks. Useful for separating work contexts or major initiatives. 13 + 14 + **List all projects**: 15 + 16 + ```sh 17 + noteleaf task projects 18 + ``` 19 + 20 + Shows each project with task count. 21 + 22 + **Filter tasks by project**: 23 + 24 + ```sh 25 + noteleaf task list --project work 26 + ``` 27 + 28 + **Project naming**: Use lowercase, hyphens for spaces. Examples: `work`, `side-project`, `home-improvement`. 29 + 30 + ## Contexts 31 + 32 + Contexts represent where or how a task can be done. Helps with GTD-style workflow. 33 + 34 + **List all contexts**: 35 + 36 + ```sh 37 + noteleaf task contexts 38 + ``` 39 + 40 + **Filter by context**: 41 + 42 + ```sh 43 + noteleaf task list --context @home 44 + ``` 45 + 46 + **Context naming**: Prefix with `@` following GTD convention. Examples: `@home`, `@office`, `@phone`, `@errands`. 47 + 48 + ## Tags 49 + 50 + Tags provide flexible categorization. Unlike projects and contexts, tasks can have multiple tags. 51 + 52 + **List all tags**: 53 + 54 + ```sh 55 + noteleaf task tags 56 + ``` 57 + 58 + **Filter by tags** (tasks must have all specified tags): 59 + 60 + ```sh 61 + noteleaf task list --tags urgent,bug 62 + ``` 63 + 64 + **Tag naming**: Use lowercase, hyphens for compound tags. Examples: `urgent`, `quick-win`, `code-review`, `waiting-on-feedback`.
+11
website/docs/tasks/overview.md
··· 1 + --- 2 + title: Task Management Overview 3 + sidebar_label: Overview 4 + description: High-level summary of Noteleaf's task workflow. 5 + sidebar_position: 1 6 + --- 7 + 8 + # Task Management Overview 9 + 10 + Noteleaf provides TaskWarrior-inspired task management with priorities, projects, contexts, tags, dependencies, and time tracking. 11 + Whether you're managing personal todos or complex projects, Noteleaf helps you organize work and track progress.
+33
website/docs/tasks/queries.md
··· 1 + --- 2 + title: Task Queries and Filtering 3 + sidebar_label: Queries 4 + description: Compose filters for precise task lists and reports. 5 + sidebar_position: 7 6 + --- 7 + 8 + # Task Queries and Filtering 9 + 10 + Combine filters for precise task lists: 11 + 12 + **High priority work tasks due this week**: 13 + 14 + ```sh 15 + noteleaf task list \ 16 + --project work \ 17 + --priority high \ 18 + --status pending 19 + ``` 20 + 21 + **All completed tasks from specific project**: 22 + 23 + ```sh 24 + noteleaf task list \ 25 + --project side-project \ 26 + --status completed 27 + ``` 28 + 29 + **Quick wins** (tasks tagged as quick): 30 + 31 + ```sh 32 + noteleaf task list --tags quick-win 33 + ```
+34
website/docs/tasks/templates.md
··· 1 + --- 2 + title: Task Templates 3 + sidebar_label: Templates 4 + description: Shell helpers for creating consistent task structures. 5 + sidebar_position: 9 6 + --- 7 + 8 + # Task Templates 9 + 10 + While templates aren't built-in, you can create shell functions for common task patterns: 11 + 12 + ```sh 13 + # In your ~/.bashrc or ~/.zshrc 14 + bug() { 15 + noteleaf task add "$1" \ 16 + --project $(git rev-parse --show-toplevel | xargs basename) \ 17 + --tags bug \ 18 + --priority high 19 + } 20 + 21 + meeting() { 22 + noteleaf task add "$1" \ 23 + --project work \ 24 + --context @office \ 25 + --recur "FREQ=WEEKLY;BYDAY=$2" 26 + } 27 + ``` 28 + 29 + Usage: 30 + 31 + ```sh 32 + bug "Fix login redirect" 33 + meeting "Sprint planning" "MO" 34 + ```
+60
website/docs/tasks/time-tracking.md
··· 1 + --- 2 + title: Time Tracking 3 + sidebar_label: Time Tracking 4 + description: Track work, review sessions, and generate timesheets. 5 + sidebar_position: 4 6 + --- 7 + 8 + # Time Tracking 9 + 10 + Track hours spent on tasks for billing, reporting, or personal analytics. 11 + 12 + ## Starting and Stopping 13 + 14 + **Start tracking**: 15 + 16 + ```sh 17 + noteleaf task start 1 18 + ``` 19 + 20 + With a note about what you're doing: 21 + 22 + ```sh 23 + noteleaf task start 1 --note "Implementing authentication" 24 + ``` 25 + 26 + **Stop tracking**: 27 + 28 + ```sh 29 + noteleaf task stop 1 30 + ``` 31 + 32 + Only one task can be actively tracked at a time. 33 + 34 + ## Viewing Timesheets 35 + 36 + **Last 7 days** (default): 37 + 38 + ```sh 39 + noteleaf task timesheet 40 + ``` 41 + 42 + **Specific time range**: 43 + 44 + ```sh 45 + noteleaf task timesheet --days 30 46 + ``` 47 + 48 + **For specific task**: 49 + 50 + ```sh 51 + noteleaf task timesheet --task 1 52 + ``` 53 + 54 + Timesheet shows: 55 + 56 + - Date and time range for each session 57 + - Duration 58 + - Notes attached to the session 59 + - Total time per task 60 + - Total time across all tasks
+297
website/docs/workflows/import-export.md
··· 1 + --- 2 + title: Import and Export 3 + sidebar_label: Import & Export 4 + sidebar_position: 1 5 + description: Data portability, backups, and migration. 6 + --- 7 + 8 + # Import and Export 9 + 10 + Noteleaf stores data in open formats for portability: SQLite for structured data and Markdown for notes. 11 + 12 + ## Data Storage 13 + 14 + ### SQLite Database 15 + 16 + Location varies by platform: 17 + 18 + **macOS:** 19 + 20 + ``` 21 + ~/Library/Application Support/noteleaf/noteleaf.db 22 + ``` 23 + 24 + **Linux:** 25 + 26 + ``` 27 + ~/.local/share/noteleaf/noteleaf.db 28 + ``` 29 + 30 + **Windows:** 31 + 32 + ``` 33 + %LOCALAPPDATA%\noteleaf\noteleaf.db 34 + ``` 35 + 36 + ### Markdown Files 37 + 38 + Notes are stored as individual markdown files: 39 + 40 + **Default location:** 41 + 42 + ``` 43 + <data_dir>/notes/ 44 + ``` 45 + 46 + Configure via `notes_dir` in `.noteleaf.conf.toml`. 47 + 48 + ### Articles 49 + 50 + Saved articles are stored as markdown: 51 + 52 + **Default location:** 53 + 54 + ``` 55 + <data_dir>/articles/ 56 + ``` 57 + 58 + Configure via `articles_dir` in `.noteleaf.conf.toml`. 59 + 60 + ## JSON Export 61 + 62 + ### Task Export 63 + 64 + Export tasks to JSON format: 65 + 66 + ```sh 67 + noteleaf todo view 123 --json 68 + noteleaf todo list --static --json 69 + ``` 70 + 71 + Output includes all task attributes: 72 + 73 + - Description 74 + - Status, priority 75 + - Project, context, tags 76 + - Due dates, recurrence 77 + - Dependencies, parent tasks 78 + - Timestamps 79 + 80 + ### Export Format Configuration 81 + 82 + Set default export format: 83 + 84 + ```sh 85 + noteleaf config set export_format "json" 86 + ``` 87 + 88 + Options: 89 + 90 + - `json` (default) 91 + - `csv` (planned) 92 + - `markdown` (planned) 93 + 94 + ## Backup Strategy 95 + 96 + ### Full Backup 97 + 98 + Back up the entire data directory: 99 + 100 + ```sh 101 + # macOS 102 + cp -r ~/Library/Application\ Support/noteleaf ~/Backups/noteleaf-$(date +%Y%m%d) 103 + 104 + # Linux 105 + cp -r ~/.local/share/noteleaf ~/backups/noteleaf-$(date +%Y%m%d) 106 + ``` 107 + 108 + Includes: 109 + 110 + - SQLite database 111 + - Notes directory 112 + - Articles directory 113 + - Configuration file 114 + 115 + ### Database Only 116 + 117 + ```sh 118 + # macOS 119 + cp ~/Library/Application\ Support/noteleaf/noteleaf.db ~/Backups/ 120 + 121 + # Linux 122 + cp ~/.local/share/noteleaf/noteleaf.db ~/backups/ 123 + ``` 124 + 125 + ### Notes Only 126 + 127 + ```sh 128 + # Copy notes directory 129 + cp -r <data_dir>/notes ~/Backups/notes-$(date +%Y%m%d) 130 + ``` 131 + 132 + Notes are plain markdown files, easily versioned with Git: 133 + 134 + ```sh 135 + cd <data_dir>/notes 136 + git init 137 + git add . 138 + git commit -m "Initial notes backup" 139 + ``` 140 + 141 + ## Restore from Backup 142 + 143 + ### Full Restore 144 + 145 + ```sh 146 + # Stop noteleaf 147 + # Replace data directory 148 + cp -r ~/Backups/noteleaf-20240315 ~/Library/Application\ Support/noteleaf 149 + ``` 150 + 151 + ### Database Restore 152 + 153 + ```sh 154 + cp ~/Backups/noteleaf.db ~/Library/Application\ Support/noteleaf/ 155 + ``` 156 + 157 + ### Notes Restore 158 + 159 + ```sh 160 + cp -r ~/Backups/notes-20240315 <data_dir>/notes 161 + ``` 162 + 163 + ## Direct Database Access 164 + 165 + SQLite database is accessible with standard tools: 166 + 167 + ```sh 168 + # Open database 169 + sqlite3 ~/Library/Application\ Support/noteleaf/noteleaf.db 170 + 171 + # List tables 172 + .tables 173 + 174 + # Query tasks 175 + SELECT id, description, status FROM tasks WHERE status = 'pending'; 176 + 177 + # Export to CSV 178 + .mode csv 179 + .output tasks.csv 180 + SELECT * FROM tasks; 181 + .quit 182 + ``` 183 + 184 + ## Portable Installation 185 + 186 + Use environment variables for portable setup: 187 + 188 + ```sh 189 + export NOTELEAF_DATA_DIR=/path/to/usb/noteleaf-data 190 + export NOTELEAF_CONFIG=/path/to/usb/noteleaf.conf.toml 191 + noteleaf todo list 192 + ``` 193 + 194 + Useful for: 195 + 196 + - USB drive installations 197 + - Synced folders (Dropbox, iCloud) 198 + - Multiple workspaces 199 + - Testing environments 200 + 201 + ## Migration Strategies 202 + 203 + ### From TaskWarrior 204 + 205 + Manual migration via SQLite: 206 + 207 + 1. Export TaskWarrior data to JSON 208 + 2. Parse JSON and insert into noteleaf database 209 + 3. Map TaskWarrior attributes to Noteleaf schema 210 + 211 + Custom migration script required (future documentation). 212 + 213 + ### From todo.txt 214 + 215 + Convert todo.txt to Noteleaf tasks: 216 + 217 + 1. Parse todo.txt format 218 + 2. Map projects, contexts, priorities 219 + 3. Bulk insert via SQLite 220 + 221 + Custom migration script required (future documentation). 222 + 223 + ### From Other Note Apps 224 + 225 + Notes are markdown files: 226 + 227 + 1. Export notes from source app 228 + 2. Convert to plain markdown 229 + 3. Copy to `<data_dir>/notes/` 230 + 4. Noteleaf will index them on next scan 231 + 232 + ## Sync and Cloud Storage 233 + 234 + ### Cloud Sync 235 + 236 + Store data directory in synced folder: 237 + 238 + ```sh 239 + # Use Dropbox 240 + export NOTELEAF_DATA_DIR=~/Dropbox/noteleaf-data 241 + 242 + # Use iCloud 243 + export NOTELEAF_DATA_DIR=~/Library/Mobile\ Documents/com~apple~CloudDocs/noteleaf 244 + ``` 245 + 246 + **Warning:** SQLite databases don't handle concurrent writes well. Only run one Noteleaf instance at a time per database. 247 + 248 + ### Version Control 249 + 250 + Notes directory can be versioned: 251 + 252 + ```sh 253 + cd <data_dir>/notes 254 + git init 255 + git add . 256 + git commit -m "Initial commit" 257 + git remote add origin <repository-url> 258 + git push -u origin main 259 + ``` 260 + 261 + Automatic git commits planned for future release. 262 + 263 + ## Data Formats 264 + 265 + ### SQLite Schema 266 + 267 + View schema: 268 + 269 + ```sh 270 + sqlite3 noteleaf.db .schema 271 + ``` 272 + 273 + Tables include: 274 + 275 + - `tasks` - Task management 276 + - `notes` - Note metadata 277 + - `articles` - Article metadata 278 + - `books`, `movies`, `tv_shows` - Media tracking 279 + - `publications` - Leaflet.pub publications 280 + - Linking tables for tags, dependencies 281 + 282 + ### Markdown Format 283 + 284 + Notes use standard markdown with YAML frontmatter: 285 + 286 + ```markdown 287 + --- 288 + title: Note Title 289 + created: 2024-03-15T10:30:00Z 290 + modified: 2024-03-15T11:00:00Z 291 + tags: [tag1, tag2] 292 + --- 293 + 294 + # Note Content 295 + 296 + Regular markdown content... 297 + ```
+62 -75
website/docusaurus.config.ts
··· 1 + import type { SidebarsConfig } from "@docusaurus/plugin-content-docs"; 2 + import type * as Preset from "@docusaurus/preset-classic"; 3 + import type { Config } from "@docusaurus/types"; 1 4 import { themes as prismThemes } from "prism-react-renderer"; 2 - import type { Config } from "@docusaurus/types"; 3 - import type * as Preset from "@docusaurus/preset-classic"; 4 5 5 6 const ghURL = "https://github.com/stormlightlabs/noteleaf"; 6 7 // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) 8 + const pragmaticSidebar: SidebarsConfig[string] = [ 9 + { type: "doc", id: "quickstart" }, 10 + { type: "category", label: "Concepts", items: [{ type: "autogenerated", dirName: "concepts" }] }, 11 + { type: "doc", id: "configuration" }, 12 + { type: "category", label: "Tasks", items: [{ type: "autogenerated", dirName: "tasks" }] }, 13 + { type: "category", label: "Notes", items: [{ type: "autogenerated", dirName: "notes" }] }, 14 + { type: "category", label: "Leaflet.pub", items: [{ type: "autogenerated", dirName: "leaflet" }] }, 15 + { type: "category", label: "Media", items: [{ type: "autogenerated", dirName: "media" }] }, 16 + { type: "category", label: "Articles", items: [{ type: "autogenerated", dirName: "articles" }] }, 17 + { type: "category", label: "Integrations", items: [{ type: "autogenerated", dirName: "integrations" }] }, 18 + { type: "category", label: "Workflows", items: [{ type: "autogenerated", dirName: "workflows" }] }, 19 + { type: "category", label: "Development", items: [{ type: "autogenerated", dirName: "development" }] }, 20 + ]; 21 + 7 22 const config: Config = { 8 - title: "My Site", 9 - tagline: "Dinosaurs are cool", 10 - favicon: "img/favicon.ico", 11 - // Improve compatibility with the upcoming Docusaurus v4 12 - future: { v4: true }, 13 - url: "https://stormlightlabs.github.io/", 14 - baseUrl: "/noteleaf/", 15 - organizationName: "stormlightlabs", 16 - projectName: "noteleaf", 17 - onBrokenLinks: "throw", 18 - i18n: { defaultLocale: "en", locales: ["en"] }, 19 - presets: [ 20 - [ 21 - "classic", 22 - { 23 - docs: { sidebarPath: "./sidebars.ts" }, 24 - theme: { customCss: "./src/css/custom.css" }, 25 - } satisfies Preset.Options, 26 - ], 27 - ], 23 + title: "Noteleaf", 24 + tagline: "Notes, blog posts, and productivity from the terminal", 25 + favicon: "img/logo.svg", 26 + // Improve compatibility with the upcoming Docusaurus v4 27 + future: { v4: true }, 28 + url: "https://stormlightlabs.github.io/", 29 + baseUrl: "/noteleaf/", 30 + organizationName: "stormlightlabs", 31 + projectName: "noteleaf", 32 + onBrokenLinks: "throw", 33 + i18n: { defaultLocale: "en", locales: ["en"] }, 34 + customFields: { pragmaticSidebar }, 35 + presets: [[ 36 + "classic", 37 + { docs: { sidebarPath: "./sidebars.ts" }, theme: { customCss: "./src/css/custom.css" } } satisfies Preset.Options, 38 + ]], 28 39 29 - themeConfig: { 30 - image: "img/docusaurus-social-card.jpg", 31 - colorMode: { respectPrefersColorScheme: true }, 32 - navbar: { 33 - title: "My Site", 34 - logo: { alt: "My Site Logo", src: "img/logo.svg" }, 35 - items: [ 36 - { 37 - type: "docSidebar", 38 - sidebarId: "manualSidebar", 39 - position: "left", 40 - label: "Manual", 41 - }, 42 - { href: ghURL, label: "GitHub", position: "right" }, 43 - ], 44 - }, 45 - footer: { 46 - style: "dark", 47 - links: [ 48 - { 49 - title: "Docs", 50 - items: [ 51 - { 52 - label: "Tutorial", 53 - to: "/docs/intro", 54 - }, 55 - ], 56 - }, 57 - { 58 - title: "Community", 59 - items: [ 60 - { 61 - label: "BlueSky", 62 - href: "https://bsky.app/desertthunder.dev", 63 - }, 64 - { 65 - label: "X", 66 - href: "https://x.com/_desertthunder", 67 - }, 68 - ], 69 - }, 70 - { 71 - title: "More", 72 - items: [{ label: "GitHub", href: ghURL }], 73 - }, 74 - ], 75 - copyright: `Copyright ยฉ ${new Date().getFullYear()} Stormlight Labs, LLC.`, 76 - }, 77 - prism: { 78 - theme: prismThemes.github, 79 - darkTheme: prismThemes.dracula, 80 - }, 81 - } satisfies Preset.ThemeConfig, 40 + themeConfig: { 41 + image: "img/social-card.png", 42 + colorMode: { respectPrefersColorScheme: true }, 43 + navbar: { 44 + title: "Noteleaf", 45 + logo: { alt: "Noteleaf Logo", src: "img/logo.svg" }, 46 + items: [{ type: "doc", docId: "quickstart", position: "left", label: "Quickstart" }, { 47 + type: "docSidebar", 48 + sidebarId: "manualSidebar", 49 + position: "left", 50 + label: "Manual", 51 + }, { href: ghURL, label: "GitHub", position: "right" }], 52 + }, 53 + footer: { 54 + style: "dark", 55 + links: [{ 56 + title: "Docs", 57 + items: [{ label: "Quickstart", to: "/docs/quickstart" }, { label: "Manual", to: "/docs/concepts/overview" }], 58 + }, { 59 + title: "Community", 60 + items: [{ label: "BlueSky", href: "https://bsky.app/desertthunder.dev" }, { 61 + label: "X", 62 + href: "https://x.com/_desertthunder", 63 + }], 64 + }, { title: "More", items: [{ label: "GitHub", href: ghURL }] }], 65 + copyright: `Copyright ยฉ ${new Date().getFullYear()} Stormlight Labs, LLC.`, 66 + }, 67 + prism: { theme: prismThemes.github, darkTheme: prismThemes.dracula }, 68 + } satisfies Preset.ThemeConfig, 82 69 }; 83 70 84 71 export default config;
+6
website/dprint.json
··· 1 + { 2 + "typescript": { "preferSingleLine": true, "jsx.bracketPosition": "sameLine" }, 3 + "json": { "preferSingleLine": true, "lineWidth": 121, "indentWidth": 2 }, 4 + "excludes": ["**/node_modules"], 5 + "plugins": ["https://plugins.dprint.dev/typescript-0.95.8.wasm", "https://plugins.dprint.dev/json-0.20.0.wasm"] 6 + }
+152
website/generate-social-card.mjs
··· 1 + import satori from 'satori'; 2 + import { html } from 'satori-html'; 3 + import sharp from 'sharp'; 4 + import { readFile, writeFile } from 'fs/promises'; 5 + 6 + // Color palette from internal/ui/palette.go 7 + const colors = { 8 + bgBase: '#201F26', // Pepper 9 + bgSecondary: '#2d2c35', // BBQ 10 + textPrimary: '#F1EFEF', // Salt 11 + textSecondary: '#BFBCC8', // Smoke 12 + primary: '#00A4FF', // Malibu 13 + success: '#00FFB2', // Julep 14 + accent: '#5CDFEA', // Lichen 15 + warning: '#FF985A', // Tang 16 + }; 17 + 18 + async function generateSocialCard() { 19 + // Load bundled font from fontsource 20 + const fontData = await readFile('./node_modules/@fontsource/google-sans-code/files/google-sans-code-latin-400-normal.woff'); 21 + 22 + const markup = html` 23 + <div 24 + style=" 25 + display: flex; 26 + flex-direction: column; 27 + width: 100%; 28 + height: 100%; 29 + background: ${colors.bgBase}; 30 + padding: 60px 80px; 31 + font-family: 'Google Sans Code'; 32 + " 33 + > 34 + <div style=" 35 + display: flex; 36 + flex-direction: column; 37 + flex: 1; 38 + justify-content: center; 39 + "> 40 + <div style=" 41 + display: flex; 42 + align-items: center; 43 + margin-bottom: 40px; 44 + "> 45 + <span style=" 46 + color: ${colors.success}; 47 + font-size: 32px; 48 + margin-right: 16px; 49 + ">$</span> 50 + <span style=" 51 + color: ${colors.textPrimary}; 52 + font-size: 32px; 53 + ">noteleaf --help</span> 54 + </div> 55 + 56 + <div style=" 57 + display: flex; 58 + flex-direction: column; 59 + background: ${colors.bgSecondary}; 60 + border-left: 4px solid ${colors.primary}; 61 + padding: 40px; 62 + border-radius: 8px; 63 + "> 64 + <div style=" 65 + color: ${colors.primary}; 66 + font-size: 72px; 67 + font-weight: bold; 68 + margin-bottom: 24px; 69 + ">noteleaf</div> 70 + 71 + <div style=" 72 + color: ${colors.textSecondary}; 73 + font-size: 36px; 74 + line-height: 1.5; 75 + "> 76 + A terminal-based productivity system for tasks, notes, and media tracking 77 + </div> 78 + </div> 79 + 80 + <div style=" 81 + display: flex; 82 + margin-top: 40px; 83 + gap: 40px; 84 + "> 85 + <div style="display: flex; align-items: center;"> 86 + <span style="color: ${colors.accent}; font-size: 24px; margin-right: 8px;">></span> 87 + <span style="color: ${colors.textPrimary}; font-size: 24px;">Tasks</span> 88 + </div> 89 + <div style="display: flex; align-items: center;"> 90 + <span style="color: ${colors.accent}; font-size: 24px; margin-right: 8px;">></span> 91 + <span style="color: ${colors.textPrimary}; font-size: 24px;">Notes</span> 92 + </div> 93 + <div style="display: flex; align-items: center;"> 94 + <span style="color: ${colors.accent}; font-size: 24px; margin-right: 8px;">></span> 95 + <span style="color: ${colors.textPrimary}; font-size: 24px;">Media</span> 96 + </div> 97 + </div> 98 + </div> 99 + 100 + <div style=" 101 + display: flex; 102 + flex-direction: column; 103 + gap: 8px; 104 + margin-top: 40px; 105 + padding-top: 20px; 106 + border-top: 1px solid ${colors.bgSecondary}; 107 + "> 108 + <div style=" 109 + display: flex; 110 + justify-content: space-between; 111 + align-items: center; 112 + "> 113 + <div style="color: ${colors.textSecondary}; font-size: 20px;"> 114 + stormlightlabs.github.io/noteleaf 115 + </div> 116 + <div style=" 117 + color: ${colors.success}; 118 + font-size: 20px; 119 + ">Open Source</div> 120 + </div> 121 + <div style="color: ${colors.accent}; font-size: 20px;"> 122 + tangled.org/@desertthunder.dev/noteleaf 123 + </div> 124 + </div> 125 + </div> 126 + `; 127 + 128 + const svg = await satori(markup, { 129 + width: 1200, 130 + height: 630, 131 + fonts: [ 132 + { 133 + name: 'Google Sans Code', 134 + data: fontData, 135 + weight: 400, 136 + style: 'normal', 137 + }, 138 + ], 139 + }); 140 + 141 + // Convert SVG to PNG using sharp 142 + const pngBuffer = await sharp(Buffer.from(svg)) 143 + .png() 144 + .toBuffer(); 145 + 146 + // Write the PNG file 147 + await writeFile('./static/img/social-card.png', pngBuffer); 148 + 149 + console.log('Social card generated successfully: static/img/social-card.png'); 150 + } 151 + 152 + generateSocialCard().catch(console.error);
+50 -45
website/package.json
··· 1 1 { 2 - "name": "noteleaf-website", 3 - "version": "0.0.0", 4 - "private": true, 5 - "scripts": { 6 - "docusaurus": "docusaurus", 7 - "start": "docusaurus start", 8 - "build": "docusaurus build", 9 - "swizzle": "docusaurus swizzle", 10 - "deploy": "docusaurus deploy", 11 - "clear": "docusaurus clear", 12 - "serve": "docusaurus serve", 13 - "write-translations": "docusaurus write-translations", 14 - "write-heading-ids": "docusaurus write-heading-ids", 15 - "typecheck": "tsc" 16 - }, 17 - "dependencies": { 18 - "@docusaurus/core": "3.9.1", 19 - "@docusaurus/preset-classic": "3.9.1", 20 - "@mdx-js/react": "^3.0.0", 21 - "clsx": "^2.0.0", 22 - "prism-react-renderer": "^2.3.0", 23 - "react": "^19.0.0", 24 - "react-dom": "^19.0.0" 25 - }, 26 - "devDependencies": { 27 - "@docusaurus/module-type-aliases": "3.9.1", 28 - "@docusaurus/tsconfig": "3.9.1", 29 - "@docusaurus/types": "3.9.1", 30 - "typescript": "~5.6.2" 31 - }, 32 - "browserslist": { 33 - "production": [ 34 - ">0.5%", 35 - "not dead", 36 - "not op_mini all" 37 - ], 38 - "development": [ 39 - "last 3 chrome version", 40 - "last 3 firefox version", 41 - "last 5 safari version" 42 - ] 43 - }, 44 - "engines": { 45 - "node": ">=20.0" 46 - } 2 + "name": "noteleaf-website", 3 + "version": "0.0.0", 4 + "private": true, 5 + "scripts": { 6 + "docusaurus": "docusaurus", 7 + "start": "docusaurus start", 8 + "build": "docusaurus build", 9 + "swizzle": "docusaurus swizzle", 10 + "deploy": "docusaurus deploy", 11 + "clear": "docusaurus clear", 12 + "serve": "docusaurus serve", 13 + "write-translations": "docusaurus write-translations", 14 + "write-heading-ids": "docusaurus write-heading-ids", 15 + "typecheck": "tsc", 16 + "generate-social-card": "node generate-social-card.mjs" 17 + }, 18 + "dependencies": { 19 + "@docusaurus/core": "3.9.1", 20 + "@docusaurus/preset-classic": "3.9.1", 21 + "@mdx-js/react": "^3.0.0", 22 + "clsx": "^2.0.0", 23 + "prism-react-renderer": "^2.3.0", 24 + "react": "^19.0.0", 25 + "react-dom": "^19.0.0", 26 + "satori": "^0.14.1", 27 + "satori-html": "^0.3.2", 28 + "sharp": "^0.33.5" 29 + }, 30 + "devDependencies": { 31 + "@docusaurus/module-type-aliases": "3.9.1", 32 + "@docusaurus/tsconfig": "3.9.1", 33 + "@docusaurus/types": "3.9.1", 34 + "dprint": "^0.50.2", 35 + "typescript": "~5.6.2" 36 + }, 37 + "browserslist": { 38 + "production": [ 39 + ">0.5%", 40 + "not dead", 41 + "not op_mini all" 42 + ], 43 + "development": [ 44 + "last 3 chrome version", 45 + "last 3 firefox version", 46 + "last 5 safari version" 47 + ] 48 + }, 49 + "engines": { 50 + "node": ">=20.0" 51 + } 47 52 }
+496
website/pnpm-lock.yaml
··· 29 29 react-dom: 30 30 specifier: ^19.0.0 31 31 version: 19.2.0(react@19.2.0) 32 + satori: 33 + specifier: ^0.14.1 34 + version: 0.14.1 35 + satori-html: 36 + specifier: ^0.3.2 37 + version: 0.3.2 38 + sharp: 39 + specifier: ^0.33.5 40 + version: 0.33.5 32 41 devDependencies: 33 42 '@docusaurus/module-type-aliases': 34 43 specifier: 3.9.1 ··· 39 48 '@docusaurus/types': 40 49 specifier: 3.9.1 41 50 version: 3.9.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) 51 + dprint: 52 + specifier: ^0.50.2 53 + version: 0.50.2 42 54 typescript: 43 55 specifier: ~5.6.2 44 56 version: 5.6.3 ··· 1188 1200 resolution: {integrity: sha512-YAL4yhhWLl9DXuf5MVig260a6INz4MehrBGFU/CZu8yXmRiYEuQvRFWh9ZsjfAOyaG7za1MNmBVZ4VVAi/CiJA==} 1189 1201 engines: {node: '>=20.0'} 1190 1202 1203 + '@dprint/darwin-arm64@0.50.2': 1204 + resolution: {integrity: sha512-4d08INZlTxbPW9LK9W8+93viN543/qA2Kxn4azVnPW/xCb2Im03UqJBz8mMm3nJZdtNnK3uTVG3ib1VW+XJisw==} 1205 + cpu: [arm64] 1206 + os: [darwin] 1207 + 1208 + '@dprint/darwin-x64@0.50.2': 1209 + resolution: {integrity: sha512-ZXWPBwdLojhdBATq+bKwJvB7D8bIzrD6eR/Xuq9UYE7evQazUiR069d9NPF0iVuzTo6wNf9ub9SXI7qDl11EGA==} 1210 + cpu: [x64] 1211 + os: [darwin] 1212 + 1213 + '@dprint/linux-arm64-glibc@0.50.2': 1214 + resolution: {integrity: sha512-marxQzRw8atXAnaawwZHeeUaaAVewrGTlFKKcDASGyjPBhc23J5fHPUPremm8xCbgYZyTlokzrV8/1rDRWhJcw==} 1215 + cpu: [arm64] 1216 + os: [linux] 1217 + 1218 + '@dprint/linux-arm64-musl@0.50.2': 1219 + resolution: {integrity: sha512-oGDq44ydzo0ZkJk6RHcUzUN5sOMT5HC6WA8kHXI6tkAsLUkaLO2DzZFfW4aAYZUn+hYNpQfQD8iGew0sjkyLyg==} 1220 + cpu: [arm64] 1221 + os: [linux] 1222 + 1223 + '@dprint/linux-riscv64-glibc@0.50.2': 1224 + resolution: {integrity: sha512-QMmZoZYWsXezDcC03fBOwPfxhTpPEyHqutcgJ0oauN9QcSXGji9NSZITMmtLz2Ki3T1MIvdaLd1goGzNSvNqTQ==} 1225 + cpu: [riscv64] 1226 + os: [linux] 1227 + 1228 + '@dprint/linux-x64-glibc@0.50.2': 1229 + resolution: {integrity: sha512-KMeHEzb4teQJChTgq8HuQzc+reRNDnarOTGTQovAZ9WNjOtKLViftsKWW5HsnRHtP5nUIPE9rF1QLjJ/gUsqvw==} 1230 + cpu: [x64] 1231 + os: [linux] 1232 + 1233 + '@dprint/linux-x64-musl@0.50.2': 1234 + resolution: {integrity: sha512-qM37T7H69g5coBTfE7SsA+KZZaRBky6gaUhPgAYxW+fOsoVtZSVkXtfTtQauHTpqqOEtbxfCtum70Hz1fr1teg==} 1235 + cpu: [x64] 1236 + os: [linux] 1237 + 1238 + '@dprint/win32-arm64@0.50.2': 1239 + resolution: {integrity: sha512-kuGVHGoxLwssVDsodefUIYQRoO2fQncurH/xKgXiZwMPOSzFcgUzYJQiyqmJEp+PENhO9VT1hXUHZtlyCAWBUQ==} 1240 + cpu: [arm64] 1241 + os: [win32] 1242 + 1243 + '@dprint/win32-x64@0.50.2': 1244 + resolution: {integrity: sha512-N3l9k31c3IMfVXqL0L6ygIhJFvCIrfQ+Z5Jph6RnCcBO6oDYWeYhAv/qBk1vLsF2y/e79TKsR1tvaEwnrQ03XA==} 1245 + cpu: [x64] 1246 + os: [win32] 1247 + 1248 + '@emnapi/runtime@1.7.0': 1249 + resolution: {integrity: sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==} 1250 + 1191 1251 '@hapi/hoek@9.3.0': 1192 1252 resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} 1193 1253 1194 1254 '@hapi/topo@5.1.0': 1195 1255 resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} 1196 1256 1257 + '@img/sharp-darwin-arm64@0.33.5': 1258 + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} 1259 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 1260 + cpu: [arm64] 1261 + os: [darwin] 1262 + 1263 + '@img/sharp-darwin-x64@0.33.5': 1264 + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} 1265 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 1266 + cpu: [x64] 1267 + os: [darwin] 1268 + 1269 + '@img/sharp-libvips-darwin-arm64@1.0.4': 1270 + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} 1271 + cpu: [arm64] 1272 + os: [darwin] 1273 + 1274 + '@img/sharp-libvips-darwin-x64@1.0.4': 1275 + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} 1276 + cpu: [x64] 1277 + os: [darwin] 1278 + 1279 + '@img/sharp-libvips-linux-arm64@1.0.4': 1280 + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} 1281 + cpu: [arm64] 1282 + os: [linux] 1283 + 1284 + '@img/sharp-libvips-linux-arm@1.0.5': 1285 + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} 1286 + cpu: [arm] 1287 + os: [linux] 1288 + 1289 + '@img/sharp-libvips-linux-s390x@1.0.4': 1290 + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} 1291 + cpu: [s390x] 1292 + os: [linux] 1293 + 1294 + '@img/sharp-libvips-linux-x64@1.0.4': 1295 + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} 1296 + cpu: [x64] 1297 + os: [linux] 1298 + 1299 + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': 1300 + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} 1301 + cpu: [arm64] 1302 + os: [linux] 1303 + 1304 + '@img/sharp-libvips-linuxmusl-x64@1.0.4': 1305 + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} 1306 + cpu: [x64] 1307 + os: [linux] 1308 + 1309 + '@img/sharp-linux-arm64@0.33.5': 1310 + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} 1311 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 1312 + cpu: [arm64] 1313 + os: [linux] 1314 + 1315 + '@img/sharp-linux-arm@0.33.5': 1316 + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} 1317 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 1318 + cpu: [arm] 1319 + os: [linux] 1320 + 1321 + '@img/sharp-linux-s390x@0.33.5': 1322 + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} 1323 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 1324 + cpu: [s390x] 1325 + os: [linux] 1326 + 1327 + '@img/sharp-linux-x64@0.33.5': 1328 + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} 1329 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 1330 + cpu: [x64] 1331 + os: [linux] 1332 + 1333 + '@img/sharp-linuxmusl-arm64@0.33.5': 1334 + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} 1335 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 1336 + cpu: [arm64] 1337 + os: [linux] 1338 + 1339 + '@img/sharp-linuxmusl-x64@0.33.5': 1340 + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} 1341 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 1342 + cpu: [x64] 1343 + os: [linux] 1344 + 1345 + '@img/sharp-wasm32@0.33.5': 1346 + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} 1347 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 1348 + cpu: [wasm32] 1349 + 1350 + '@img/sharp-win32-ia32@0.33.5': 1351 + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} 1352 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 1353 + cpu: [ia32] 1354 + os: [win32] 1355 + 1356 + '@img/sharp-win32-x64@0.33.5': 1357 + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} 1358 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 1359 + cpu: [x64] 1360 + os: [win32] 1361 + 1197 1362 '@jest/schemas@29.6.3': 1198 1363 resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} 1199 1364 engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} ··· 1299 1464 1300 1465 '@polka/url@1.0.0-next.29': 1301 1466 resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} 1467 + 1468 + '@shuding/opentype.js@1.4.0-beta.0': 1469 + resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==} 1470 + engines: {node: '>= 8.0.0'} 1471 + hasBin: true 1302 1472 1303 1473 '@sideway/address@4.1.5': 1304 1474 resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} ··· 1778 1948 balanced-match@1.0.2: 1779 1949 resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 1780 1950 1951 + base64-js@0.0.8: 1952 + resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} 1953 + engines: {node: '>= 0.4'} 1954 + 1781 1955 baseline-browser-mapping@2.8.11: 1782 1956 resolution: {integrity: sha512-i+sRXGhz4+QW8aACZ3+r1GAKMt0wlFpeA8M5rOQd0HEYw9zhDrlx9Wc8uQ0IdXakjJRthzglEwfB/yqIjO6iDg==} 1783 1957 hasBin: true ··· 1872 2046 resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} 1873 2047 engines: {node: '>=14.16'} 1874 2048 2049 + camelize@1.0.1: 2050 + resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} 2051 + 1875 2052 caniuse-api@3.0.0: 1876 2053 resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} 1877 2054 ··· 1958 2135 color-name@1.1.4: 1959 2136 resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 1960 2137 2138 + color-string@1.9.1: 2139 + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} 2140 + 2141 + color@4.2.3: 2142 + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} 2143 + engines: {node: '>=12.5.0'} 2144 + 1961 2145 colord@2.9.3: 1962 2146 resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} 1963 2147 ··· 2076 2260 resolution: {integrity: sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==} 2077 2261 engines: {node: '>=12'} 2078 2262 2263 + css-background-parser@0.1.0: 2264 + resolution: {integrity: sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==} 2265 + 2079 2266 css-blank-pseudo@7.0.1: 2080 2267 resolution: {integrity: sha512-jf+twWGDf6LDoXDUode+nc7ZlrqfaNphrBIBrcmeP3D8yw1uPaix1gCC8LUQUGQ6CycuK2opkbFFWFuq/a94ag==} 2081 2268 engines: {node: '>=18'} 2082 2269 peerDependencies: 2083 2270 postcss: ^8.4 2084 2271 2272 + css-box-shadow@1.0.0-3: 2273 + resolution: {integrity: sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==} 2274 + 2275 + css-color-keywords@1.0.0: 2276 + resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} 2277 + engines: {node: '>=4'} 2278 + 2085 2279 css-declaration-sorter@7.3.0: 2086 2280 resolution: {integrity: sha512-LQF6N/3vkAMYF4xoHLJfG718HRJh34Z8BnNhd6bosOMIVjMlhuZK5++oZa3uYAgrI5+7x2o27gUqTR2U/KjUOQ==} 2087 2281 engines: {node: ^14 || ^16 || >=18} 2088 2282 peerDependencies: 2089 2283 postcss: ^8.0.9 2284 + 2285 + css-gradient-parser@0.0.16: 2286 + resolution: {integrity: sha512-3O5QdqgFRUbXvK1x5INf1YkBz1UKSWqrd63vWsum8MNHDBYD5urm3QtxZbKU259OrEXNM26lP/MPY3d1IGkBgA==} 2287 + engines: {node: '>=16'} 2090 2288 2091 2289 css-has-pseudo@7.0.3: 2092 2290 resolution: {integrity: sha512-oG+vKuGyqe/xvEMoxAQrhi7uY16deJR3i7wwhBerVrGQKSqUC5GiOVxTpM9F9B9hw0J+eKeOWLH7E9gZ1Dr5rA==} ··· 2142 2340 2143 2341 css-select@5.2.2: 2144 2342 resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} 2343 + 2344 + css-to-react-native@3.2.0: 2345 + resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} 2145 2346 2146 2347 css-tree@2.2.1: 2147 2348 resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} ··· 2273 2474 resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} 2274 2475 engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} 2275 2476 2477 + detect-libc@2.1.2: 2478 + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} 2479 + engines: {node: '>=8'} 2480 + 2276 2481 detect-node@2.1.0: 2277 2482 resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} 2278 2483 ··· 2325 2530 resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} 2326 2531 engines: {node: '>=10'} 2327 2532 2533 + dprint@0.50.2: 2534 + resolution: {integrity: sha512-+0Fzg+17jsMMUouK00/Fara5YtGOuE76EAJINHB8VpkXHd0n00rMXtw/03qorOgz23eo8Y0UpYvNZBJJo3aNtw==} 2535 + hasBin: true 2536 + 2328 2537 dunder-proto@1.0.1: 2329 2538 resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} 2330 2539 engines: {node: '>= 0.4'} ··· 2341 2550 electron-to-chromium@1.5.230: 2342 2551 resolution: {integrity: sha512-A6A6Fd3+gMdaed9wX83CvHYJb4UuapPD5X5SLq72VZJzxHSY0/LUweGXRWmQlh2ln7KV7iw7jnwXK7dlPoOnHQ==} 2343 2552 2553 + emoji-regex-xs@2.0.1: 2554 + resolution: {integrity: sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==} 2555 + engines: {node: '>=10.0.0'} 2556 + 2344 2557 emoji-regex@8.0.0: 2345 2558 resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 2346 2559 ··· 2541 2754 resolution: {integrity: sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==} 2542 2755 engines: {node: '>=0.4.0'} 2543 2756 2757 + fflate@0.7.4: 2758 + resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==} 2759 + 2544 2760 figures@3.2.0: 2545 2761 resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} 2546 2762 engines: {node: '>=8'} ··· 2734 2950 resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} 2735 2951 hasBin: true 2736 2952 2953 + hex-rgb@4.3.0: 2954 + resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} 2955 + engines: {node: '>=6'} 2956 + 2737 2957 history@4.10.1: 2738 2958 resolution: {integrity: sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==} 2739 2959 ··· 2897 3117 2898 3118 is-arrayish@0.2.1: 2899 3119 resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} 3120 + 3121 + is-arrayish@0.3.4: 3122 + resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} 2900 3123 2901 3124 is-binary-path@2.1.0: 2902 3125 resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} ··· 3107 3330 resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} 3108 3331 engines: {node: '>=14'} 3109 3332 3333 + linebreak@1.1.0: 3334 + resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} 3335 + 3110 3336 lines-and-columns@1.2.4: 3111 3337 resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} 3112 3338 ··· 3585 3811 resolution: {integrity: sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==} 3586 3812 engines: {node: '>=14.16'} 3587 3813 3814 + pako@0.2.9: 3815 + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} 3816 + 3588 3817 param-case@3.0.4: 3589 3818 resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} 3590 3819 3591 3820 parent-module@1.0.1: 3592 3821 resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} 3593 3822 engines: {node: '>=6'} 3823 + 3824 + parse-css-color@0.2.1: 3825 + resolution: {integrity: sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==} 3594 3826 3595 3827 parse-entities@4.0.2: 3596 3828 resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} ··· 4316 4548 safer-buffer@2.1.2: 4317 4549 resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} 4318 4550 4551 + satori-html@0.3.2: 4552 + resolution: {integrity: sha512-wjTh14iqADFKDK80e51/98MplTGfxz2RmIzh0GqShlf4a67+BooLywF17TvJPD6phO0Hxm7Mf1N5LtRYvdkYRA==} 4553 + 4554 + satori@0.14.1: 4555 + resolution: {integrity: sha512-BEiQzv1Mn3QpNzBvMtlZUQfby8iYt/RXvLfxBSwQpGaQjAmCNhzfgN6XZWWcTQM6j2k3r2LCxD+0heNAteg5Ng==} 4556 + engines: {node: '>=16'} 4557 + 4319 4558 sax@1.4.1: 4320 4559 resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} 4321 4560 ··· 4395 4634 shallowequal@1.1.0: 4396 4635 resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} 4397 4636 4637 + sharp@0.33.5: 4638 + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} 4639 + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 4640 + 4398 4641 shebang-command@2.0.0: 4399 4642 resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 4400 4643 engines: {node: '>=8'} ··· 4425 4668 4426 4669 signal-exit@3.0.7: 4427 4670 resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} 4671 + 4672 + simple-swizzle@0.2.4: 4673 + resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} 4428 4674 4429 4675 sirv@2.0.4: 4430 4676 resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} ··· 4511 4757 resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} 4512 4758 engines: {node: '>=12'} 4513 4759 4760 + string.prototype.codepointat@0.2.1: 4761 + resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==} 4762 + 4514 4763 string_decoder@1.1.1: 4515 4764 resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} 4516 4765 ··· 4623 4872 thunky@1.1.0: 4624 4873 resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==} 4625 4874 4875 + tiny-inflate@1.0.3: 4876 + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} 4877 + 4626 4878 tiny-invariant@1.3.3: 4627 4879 resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} 4628 4880 ··· 4684 4936 engines: {node: '>=14.17'} 4685 4937 hasBin: true 4686 4938 4939 + ultrahtml@1.6.0: 4940 + resolution: {integrity: sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==} 4941 + 4687 4942 undici-types@7.13.0: 4688 4943 resolution: {integrity: sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==} 4689 4944 ··· 4706 4961 unicode-property-aliases-ecmascript@2.2.0: 4707 4962 resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==} 4708 4963 engines: {node: '>=4'} 4964 + 4965 + unicode-trie@2.0.0: 4966 + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} 4709 4967 4710 4968 unified@11.0.5: 4711 4969 resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} ··· 4940 5198 yocto-queue@1.2.1: 4941 5199 resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} 4942 5200 engines: {node: '>=12.20'} 5201 + 5202 + yoga-wasm-web@0.3.3: 5203 + resolution: {integrity: sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==} 4943 5204 4944 5205 zod@4.1.11: 4945 5206 resolution: {integrity: sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==} ··· 6901 7162 - uglify-js 6902 7163 - webpack-cli 6903 7164 7165 + '@dprint/darwin-arm64@0.50.2': 7166 + optional: true 7167 + 7168 + '@dprint/darwin-x64@0.50.2': 7169 + optional: true 7170 + 7171 + '@dprint/linux-arm64-glibc@0.50.2': 7172 + optional: true 7173 + 7174 + '@dprint/linux-arm64-musl@0.50.2': 7175 + optional: true 7176 + 7177 + '@dprint/linux-riscv64-glibc@0.50.2': 7178 + optional: true 7179 + 7180 + '@dprint/linux-x64-glibc@0.50.2': 7181 + optional: true 7182 + 7183 + '@dprint/linux-x64-musl@0.50.2': 7184 + optional: true 7185 + 7186 + '@dprint/win32-arm64@0.50.2': 7187 + optional: true 7188 + 7189 + '@dprint/win32-x64@0.50.2': 7190 + optional: true 7191 + 7192 + '@emnapi/runtime@1.7.0': 7193 + dependencies: 7194 + tslib: 2.8.1 7195 + optional: true 7196 + 6904 7197 '@hapi/hoek@9.3.0': {} 6905 7198 6906 7199 '@hapi/topo@5.1.0': 6907 7200 dependencies: 6908 7201 '@hapi/hoek': 9.3.0 7202 + 7203 + '@img/sharp-darwin-arm64@0.33.5': 7204 + optionalDependencies: 7205 + '@img/sharp-libvips-darwin-arm64': 1.0.4 7206 + optional: true 7207 + 7208 + '@img/sharp-darwin-x64@0.33.5': 7209 + optionalDependencies: 7210 + '@img/sharp-libvips-darwin-x64': 1.0.4 7211 + optional: true 7212 + 7213 + '@img/sharp-libvips-darwin-arm64@1.0.4': 7214 + optional: true 7215 + 7216 + '@img/sharp-libvips-darwin-x64@1.0.4': 7217 + optional: true 7218 + 7219 + '@img/sharp-libvips-linux-arm64@1.0.4': 7220 + optional: true 7221 + 7222 + '@img/sharp-libvips-linux-arm@1.0.5': 7223 + optional: true 7224 + 7225 + '@img/sharp-libvips-linux-s390x@1.0.4': 7226 + optional: true 7227 + 7228 + '@img/sharp-libvips-linux-x64@1.0.4': 7229 + optional: true 7230 + 7231 + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': 7232 + optional: true 7233 + 7234 + '@img/sharp-libvips-linuxmusl-x64@1.0.4': 7235 + optional: true 7236 + 7237 + '@img/sharp-linux-arm64@0.33.5': 7238 + optionalDependencies: 7239 + '@img/sharp-libvips-linux-arm64': 1.0.4 7240 + optional: true 7241 + 7242 + '@img/sharp-linux-arm@0.33.5': 7243 + optionalDependencies: 7244 + '@img/sharp-libvips-linux-arm': 1.0.5 7245 + optional: true 7246 + 7247 + '@img/sharp-linux-s390x@0.33.5': 7248 + optionalDependencies: 7249 + '@img/sharp-libvips-linux-s390x': 1.0.4 7250 + optional: true 7251 + 7252 + '@img/sharp-linux-x64@0.33.5': 7253 + optionalDependencies: 7254 + '@img/sharp-libvips-linux-x64': 1.0.4 7255 + optional: true 7256 + 7257 + '@img/sharp-linuxmusl-arm64@0.33.5': 7258 + optionalDependencies: 7259 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 7260 + optional: true 7261 + 7262 + '@img/sharp-linuxmusl-x64@0.33.5': 7263 + optionalDependencies: 7264 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 7265 + optional: true 7266 + 7267 + '@img/sharp-wasm32@0.33.5': 7268 + dependencies: 7269 + '@emnapi/runtime': 1.7.0 7270 + optional: true 7271 + 7272 + '@img/sharp-win32-ia32@0.33.5': 7273 + optional: true 7274 + 7275 + '@img/sharp-win32-x64@0.33.5': 7276 + optional: true 6909 7277 6910 7278 '@jest/schemas@29.6.3': 6911 7279 dependencies: ··· 7045 7413 7046 7414 '@polka/url@1.0.0-next.29': {} 7047 7415 7416 + '@shuding/opentype.js@1.4.0-beta.0': 7417 + dependencies: 7418 + fflate: 0.7.4 7419 + string.prototype.codepointat: 0.2.1 7420 + 7048 7421 '@sideway/address@4.1.5': 7049 7422 dependencies: 7050 7423 '@hapi/hoek': 9.3.0 ··· 7599 7972 7600 7973 balanced-match@1.0.2: {} 7601 7974 7975 + base64-js@0.0.8: {} 7976 + 7602 7977 baseline-browser-mapping@2.8.11: {} 7603 7978 7604 7979 batch@0.6.1: {} ··· 7720 8095 7721 8096 camelcase@7.0.1: {} 7722 8097 8098 + camelize@1.0.1: {} 8099 + 7723 8100 caniuse-api@3.0.0: 7724 8101 dependencies: 7725 8102 browserslist: 4.26.3 ··· 7813 8190 7814 8191 color-name@1.1.4: {} 7815 8192 8193 + color-string@1.9.1: 8194 + dependencies: 8195 + color-name: 1.1.4 8196 + simple-swizzle: 0.2.4 8197 + 8198 + color@4.2.3: 8199 + dependencies: 8200 + color-convert: 2.0.1 8201 + color-string: 1.9.1 8202 + 7816 8203 colord@2.9.3: {} 7817 8204 7818 8205 colorette@2.0.20: {} ··· 7921 8308 dependencies: 7922 8309 type-fest: 1.4.0 7923 8310 8311 + css-background-parser@0.1.0: {} 8312 + 7924 8313 css-blank-pseudo@7.0.1(postcss@8.5.6): 7925 8314 dependencies: 7926 8315 postcss: 8.5.6 7927 8316 postcss-selector-parser: 7.1.0 7928 8317 8318 + css-box-shadow@1.0.0-3: {} 8319 + 8320 + css-color-keywords@1.0.0: {} 8321 + 7929 8322 css-declaration-sorter@7.3.0(postcss@8.5.6): 7930 8323 dependencies: 7931 8324 postcss: 8.5.6 8325 + 8326 + css-gradient-parser@0.0.16: {} 7932 8327 7933 8328 css-has-pseudo@7.0.3(postcss@8.5.6): 7934 8329 dependencies: ··· 7981 8376 domhandler: 5.0.3 7982 8377 domutils: 3.2.2 7983 8378 nth-check: 2.1.1 8379 + 8380 + css-to-react-native@3.2.0: 8381 + dependencies: 8382 + camelize: 1.0.1 8383 + css-color-keywords: 1.0.0 8384 + postcss-value-parser: 4.2.0 7984 8385 7985 8386 css-tree@2.2.1: 7986 8387 dependencies: ··· 8113 8514 dequal@2.0.3: {} 8114 8515 8115 8516 destroy@1.2.0: {} 8517 + 8518 + detect-libc@2.1.2: {} 8116 8519 8117 8520 detect-node@2.1.0: {} 8118 8521 ··· 8182 8585 dependencies: 8183 8586 is-obj: 2.0.0 8184 8587 8588 + dprint@0.50.2: 8589 + optionalDependencies: 8590 + '@dprint/darwin-arm64': 0.50.2 8591 + '@dprint/darwin-x64': 0.50.2 8592 + '@dprint/linux-arm64-glibc': 0.50.2 8593 + '@dprint/linux-arm64-musl': 0.50.2 8594 + '@dprint/linux-riscv64-glibc': 0.50.2 8595 + '@dprint/linux-x64-glibc': 0.50.2 8596 + '@dprint/linux-x64-musl': 0.50.2 8597 + '@dprint/win32-arm64': 0.50.2 8598 + '@dprint/win32-x64': 0.50.2 8599 + 8185 8600 dunder-proto@1.0.1: 8186 8601 dependencies: 8187 8602 call-bind-apply-helpers: 1.0.2 ··· 8195 8610 ee-first@1.1.1: {} 8196 8611 8197 8612 electron-to-chromium@1.5.230: {} 8613 + 8614 + emoji-regex-xs@2.0.1: {} 8198 8615 8199 8616 emoji-regex@8.0.0: {} 8200 8617 ··· 8413 8830 feed@4.2.2: 8414 8831 dependencies: 8415 8832 xml-js: 1.6.11 8833 + 8834 + fflate@0.7.4: {} 8416 8835 8417 8836 figures@3.2.0: 8418 8837 dependencies: ··· 8679 9098 8680 9099 he@1.2.0: {} 8681 9100 9101 + hex-rgb@4.3.0: {} 9102 + 8682 9103 history@4.10.1: 8683 9104 dependencies: 8684 9105 '@babel/runtime': 7.28.4 ··· 8851 9272 8852 9273 is-arrayish@0.2.1: {} 8853 9274 9275 + is-arrayish@0.3.4: {} 9276 + 8854 9277 is-binary-path@2.1.0: 8855 9278 dependencies: 8856 9279 binary-extensions: 2.3.0 ··· 9017 9440 leven@3.1.0: {} 9018 9441 9019 9442 lilconfig@3.1.3: {} 9443 + 9444 + linebreak@1.1.0: 9445 + dependencies: 9446 + base64-js: 0.0.8 9447 + unicode-trie: 2.0.0 9020 9448 9021 9449 lines-and-columns@1.2.4: {} 9022 9450 ··· 9756 10184 registry-url: 6.0.1 9757 10185 semver: 7.7.2 9758 10186 10187 + pako@0.2.9: {} 10188 + 9759 10189 param-case@3.0.4: 9760 10190 dependencies: 9761 10191 dot-case: 3.0.4 ··· 9765 10195 dependencies: 9766 10196 callsites: 3.1.0 9767 10197 10198 + parse-css-color@0.2.1: 10199 + dependencies: 10200 + color-name: 1.1.4 10201 + hex-rgb: 4.3.0 10202 + 9768 10203 parse-entities@4.0.2: 9769 10204 dependencies: 9770 10205 '@types/unist': 2.0.11 ··· 10605 11040 10606 11041 safer-buffer@2.1.2: {} 10607 11042 11043 + satori-html@0.3.2: 11044 + dependencies: 11045 + ultrahtml: 1.6.0 11046 + 11047 + satori@0.14.1: 11048 + dependencies: 11049 + '@shuding/opentype.js': 1.4.0-beta.0 11050 + css-background-parser: 0.1.0 11051 + css-box-shadow: 1.0.0-3 11052 + css-gradient-parser: 0.0.16 11053 + css-to-react-native: 3.2.0 11054 + emoji-regex-xs: 2.0.1 11055 + escape-html: 1.0.3 11056 + linebreak: 1.1.0 11057 + parse-css-color: 0.2.1 11058 + postcss-value-parser: 4.2.0 11059 + yoga-wasm-web: 0.3.3 11060 + 10608 11061 sax@1.4.1: {} 10609 11062 10610 11063 scheduler@0.27.0: {} ··· 10718 11171 10719 11172 shallowequal@1.1.0: {} 10720 11173 11174 + sharp@0.33.5: 11175 + dependencies: 11176 + color: 4.2.3 11177 + detect-libc: 2.1.2 11178 + semver: 7.7.2 11179 + optionalDependencies: 11180 + '@img/sharp-darwin-arm64': 0.33.5 11181 + '@img/sharp-darwin-x64': 0.33.5 11182 + '@img/sharp-libvips-darwin-arm64': 1.0.4 11183 + '@img/sharp-libvips-darwin-x64': 1.0.4 11184 + '@img/sharp-libvips-linux-arm': 1.0.5 11185 + '@img/sharp-libvips-linux-arm64': 1.0.4 11186 + '@img/sharp-libvips-linux-s390x': 1.0.4 11187 + '@img/sharp-libvips-linux-x64': 1.0.4 11188 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 11189 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 11190 + '@img/sharp-linux-arm': 0.33.5 11191 + '@img/sharp-linux-arm64': 0.33.5 11192 + '@img/sharp-linux-s390x': 0.33.5 11193 + '@img/sharp-linux-x64': 0.33.5 11194 + '@img/sharp-linuxmusl-arm64': 0.33.5 11195 + '@img/sharp-linuxmusl-x64': 0.33.5 11196 + '@img/sharp-wasm32': 0.33.5 11197 + '@img/sharp-win32-ia32': 0.33.5 11198 + '@img/sharp-win32-x64': 0.33.5 11199 + 10721 11200 shebang-command@2.0.0: 10722 11201 dependencies: 10723 11202 shebang-regex: 3.0.0 ··· 10756 11235 10757 11236 signal-exit@3.0.7: {} 10758 11237 11238 + simple-swizzle@0.2.4: 11239 + dependencies: 11240 + is-arrayish: 0.3.4 11241 + 10759 11242 sirv@2.0.4: 10760 11243 dependencies: 10761 11244 '@polka/url': 1.0.0-next.29 ··· 10847 11330 eastasianwidth: 0.2.0 10848 11331 emoji-regex: 9.2.2 10849 11332 strip-ansi: 7.1.2 11333 + 11334 + string.prototype.codepointat@0.2.1: {} 10850 11335 10851 11336 string_decoder@1.1.1: 10852 11337 dependencies: ··· 10951 11436 10952 11437 thunky@1.1.0: {} 10953 11438 11439 + tiny-inflate@1.0.3: {} 11440 + 10954 11441 tiny-invariant@1.3.3: {} 10955 11442 10956 11443 tiny-warning@1.0.3: {} ··· 10992 11479 10993 11480 typescript@5.6.3: {} 10994 11481 11482 + ultrahtml@1.6.0: {} 11483 + 10995 11484 undici-types@7.13.0: {} 10996 11485 10997 11486 unicode-canonical-property-names-ecmascript@2.0.1: {} ··· 11006 11495 unicode-match-property-value-ecmascript@2.2.1: {} 11007 11496 11008 11497 unicode-property-aliases-ecmascript@2.2.0: {} 11498 + 11499 + unicode-trie@2.0.0: 11500 + dependencies: 11501 + pako: 0.2.9 11502 + tiny-inflate: 1.0.3 11009 11503 11010 11504 unified@11.0.5: 11011 11505 dependencies: ··· 11311 11805 yallist@3.1.1: {} 11312 11806 11313 11807 yocto-queue@1.2.1: {} 11808 + 11809 + yoga-wasm-web@0.3.3: {} 11314 11810 11315 11811 zod@4.1.11: {} 11316 11812
+3
website/pnpm-workspace.yaml
··· 1 + onlyBuiltDependencies: 2 + - dprint 3 + - sharp
+2 -1
website/sidebars.ts
··· 1 1 import type { SidebarsConfig } from "@docusaurus/plugin-content-docs"; 2 + import config from "./docusaurus.config"; 2 3 3 4 // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) 4 5 const sidebars: SidebarsConfig = { 5 - manualSidebar: [{ type: "autogenerated", dirName: "." }], 6 + manualSidebar: (config.customFields?.pragmaticSidebar as SidebarsConfig[string]) ?? [], 6 7 }; 7 8 8 9 export default sidebars;
+67 -53
website/src/components/HomepageFeatures/index.tsx
··· 1 - import type {ReactNode} from 'react'; 2 - import clsx from 'clsx'; 3 - import Heading from '@theme/Heading'; 4 - import styles from './styles.module.css'; 1 + import Heading from "@theme/Heading"; 2 + import clsx from "clsx"; 3 + import type { ReactNode } from "react"; 4 + import styles from "./styles.module.css"; 5 + 6 + type FeatureItem = { title: string; description: ReactNode; theme: keyof typeof themeClassMap }; 5 7 6 - type FeatureItem = { 7 - title: string; 8 - Svg: React.ComponentType<React.ComponentProps<'svg'>>; 9 - description: ReactNode; 8 + const themeClassMap = { 9 + primary: styles.featureCardPrimary, 10 + info: styles.featureCardInfo, 11 + success: styles.featureCardSuccess, 12 + warning: styles.featureCardWarning, 13 + accent: styles.featureCardAccent, 14 + plum: styles.featureCardPlum, 15 + danger: styles.featureCardDanger, 10 16 }; 11 17 12 - const FeatureList: FeatureItem[] = [ 13 - { 14 - title: 'Easy to Use', 15 - Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, 16 - description: ( 17 - <> 18 - Docusaurus was designed from the ground up to be easily installed and 19 - used to get your website up and running quickly. 20 - </> 21 - ), 22 - }, 23 - { 24 - title: 'Focus on What Matters', 25 - Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default, 26 - description: ( 27 - <> 28 - Docusaurus lets you focus on your docs, and we&apos;ll do the chores. Go 29 - ahead and move your docs into the <code>docs</code> directory. 30 - </> 31 - ), 32 - }, 33 - { 34 - title: 'Powered by React', 35 - Svg: require('@site/static/img/undraw_docusaurus_react.svg').default, 36 - description: ( 37 - <> 38 - Extend or customize your website layout by reusing React. Docusaurus can 39 - be extended while reusing the same header and footer. 40 - </> 41 - ), 42 - }, 43 - ]; 18 + const FeatureList: FeatureItem[] = [{ 19 + title: "Terminal-First Interface", 20 + description: ( 21 + <>Built for the command line with a beautiful, keyboard-driven interface powered by Bubble Tea and Fang.</> 22 + ), 23 + theme: "primary", 24 + }, { 25 + title: "Task Management", 26 + description: ( 27 + <>Organize your tasks with projects, priorities, tags, contexts, due dates, and recurrenceโ€”all from a single CLI.</> 28 + ), 29 + theme: "danger", 30 + }, { 31 + title: "Leaflet.pub Publishing", 32 + description: ( 33 + <> 34 + Sync Markdown notes with leaflet.pub, push updates over AT Protocol, and manage drafts without leaving the 35 + terminal. 36 + </> 37 + ), 38 + theme: "accent", 39 + }, { 40 + title: "Articles & Readability", 41 + description: ( 42 + <> 43 + Capture the clean content of any article, store Markdown + HTML copies, and enjoy a terminal reader inspired by 44 + Readability. 45 + </> 46 + ), 47 + theme: "warning", 48 + }, { 49 + title: "Knowledge Base", 50 + description: ( 51 + <>Keep notes, track books, movies, and TV shows. Link everything together with tags, IDs, and shared metadata.</> 52 + ), 53 + theme: "info", 54 + }, { 55 + title: "Open Source & MIT Licensed", 56 + description: ( 57 + <> 58 + Built in the open on GitHub under the MIT license. Fork it, extend it, and make Noteleaf part of your own 59 + workflows. 60 + </> 61 + ), 62 + theme: "plum", 63 + }]; 44 64 45 - function Feature({title, Svg, description}: FeatureItem) { 65 + function Feature({ title, description, theme }: FeatureItem) { 66 + const cardClass = themeClassMap[theme] ?? themeClassMap.primary; 46 67 return ( 47 - <div className={clsx('col col--4')}> 48 - <div className="text--center"> 49 - <Svg className={styles.featureSvg} role="img" /> 50 - </div> 51 - <div className="text--center padding-horiz--md"> 52 - <Heading as="h3">{title}</Heading> 53 - <p>{description}</p> 68 + <div className={clsx("col col--4")}> 69 + <div className={clsx("text--left padding-horiz--md", styles.featureCard, cardClass)}> 70 + <Heading as="h3" className={styles.featureTitle}>{title}</Heading> 71 + <p className={styles.featureCopy}>{description}</p> 54 72 </div> 55 73 </div> 56 74 ); ··· 60 78 return ( 61 79 <section className={styles.features}> 62 80 <div className="container"> 63 - <div className="row"> 64 - {FeatureList.map((props, idx) => ( 65 - <Feature key={idx} {...props} /> 66 - ))} 67 - </div> 81 + <div className="row">{FeatureList.map((props, idx) => <Feature key={idx} {...props} />)}</div> 68 82 </div> 69 83 </section> 70 84 );
+99 -5
website/src/components/HomepageFeatures/styles.module.css
··· 1 1 .features { 2 2 display: flex; 3 - align-items: center; 4 - padding: 2rem 0; 3 + align-items: stretch; 4 + padding: 3rem 0; 5 5 width: 100%; 6 + background: var(--ifm-background-surface-color); 6 7 } 7 8 8 - .featureSvg { 9 - height: 200px; 10 - width: 200px; 9 + .featureCard { 10 + border-radius: 1.25rem; 11 + padding: 2rem 1.75rem; 12 + min-height: 220px; 13 + border: 1px solid var(--ifm-color-emphasis-300); 14 + background: var(--ifm-background-color); 15 + margin-bottom: 1.5rem; 16 + } 17 + 18 + .featureCardPrimary { 19 + border-top: 4px solid var(--ifm-color-primary); 20 + } 21 + 22 + .featureCardSuccess { 23 + border-top: 4px solid var(--ifm-color-success); 24 + } 25 + 26 + .featureCardInfo { 27 + border-top: 4px solid var(--ifm-color-info); 28 + } 29 + 30 + .featureCardWarning { 31 + border-top: 4px solid var(--ifm-color-warning); 32 + } 33 + 34 + .featureCardAccent { 35 + border-top: 4px solid #00ffb2; 36 + } 37 + 38 + .featureCardPlum { 39 + border-top: 4px solid #c259ff; 40 + } 41 + 42 + .featureCardDanger { 43 + border-top: 4px solid #eb4268; 44 + } 45 + 46 + .featureTitle { 47 + font-size: 1.3rem; 48 + font-weight: 700; 49 + letter-spacing: 0.04em; 50 + text-transform: uppercase; 51 + margin-bottom: 0.75rem; 52 + } 53 + 54 + .featureCopy { 55 + font-size: 1.05rem; 56 + line-height: 1.6; 57 + color: var(--ifm-font-color-secondary); 58 + margin: 0; 59 + } 60 + 61 + .featureCardPrimary .featureTitle { 62 + background: linear-gradient(120deg, var(--ifm-color-primary), #66c9ff); 63 + -webkit-background-clip: text; 64 + -webkit-text-fill-color: transparent; 65 + } 66 + 67 + .featureCardSuccess .featureTitle { 68 + background: linear-gradient(120deg, var(--ifm-color-success), #1ccb92); 69 + -webkit-background-clip: text; 70 + -webkit-text-fill-color: transparent; 71 + } 72 + 73 + .featureCardInfo .featureTitle { 74 + background: linear-gradient(120deg, var(--ifm-color-info), #8ef7f5); 75 + -webkit-background-clip: text; 76 + -webkit-text-fill-color: transparent; 77 + } 78 + 79 + .featureCardWarning .featureTitle { 80 + background: linear-gradient(120deg, var(--ifm-color-warning), #ffbd85); 81 + -webkit-background-clip: text; 82 + -webkit-text-fill-color: transparent; 83 + } 84 + 85 + .featureCardAccent .featureTitle { 86 + background: linear-gradient(120deg, #5cdf8a, #00ffb2); 87 + -webkit-background-clip: text; 88 + -webkit-text-fill-color: transparent; 89 + } 90 + 91 + .featureCardPlum .featureTitle { 92 + background: linear-gradient(120deg, #c259ff, #ff66c4); 93 + -webkit-background-clip: text; 94 + -webkit-text-fill-color: transparent; 95 + } 96 + 97 + .featureCardDanger .featureTitle { 98 + background: linear-gradient(120deg, #eb4268, #ff7b8d); 99 + -webkit-background-clip: text; 100 + -webkit-text-fill-color: transparent; 101 + } 102 + 103 + .features :global(.container) { 104 + background: transparent; 11 105 }
+74 -23
website/src/css/custom.css
··· 1 - /** 2 - * Any CSS included here will be global. The classic template 3 - * bundles Infima by default. Infima is a CSS framework designed to 4 - * work well for content-centric websites. 5 - */ 1 + 6 2 7 - /* You can override the default Infima variables here. */ 8 3 :root { 9 - --ifm-color-primary: #2e8555; 10 - --ifm-color-primary-dark: #29784c; 11 - --ifm-color-primary-darker: #277148; 12 - --ifm-color-primary-darkest: #205d3b; 13 - --ifm-color-primary-light: #33925d; 14 - --ifm-color-primary-lighter: #359962; 15 - --ifm-color-primary-lightest: #3cad6e; 4 + /* Noteleaf Color Palette - Light Mode */ 5 + --ifm-color-primary: #00A4FF; /* Malibu - Blue primary */ 6 + --ifm-color-primary-dark: #0094e6; 7 + --ifm-color-primary-darker: #008ad9; 8 + --ifm-color-primary-darkest: #0072b3; 9 + --ifm-color-primary-light: #1aadff; 10 + --ifm-color-primary-lighter: #33b7ff; 11 + --ifm-color-primary-lightest: #66c9ff; 12 + 13 + --ifm-color-success: #00FFB2; /* Julep - Green */ 14 + --ifm-color-info: #5CDFEA; /* Lichen - Teal */ 15 + --ifm-color-warning: #FF985A; /* Tang - Orange */ 16 + --ifm-color-danger: #EB4268; /* Sriracha - Red */ 17 + 18 + /* Background colors - Light mode uses lighter values */ 19 + --ifm-background-color: #FFFAF1; /* Butter */ 20 + --ifm-background-surface-color: #F1EFEF; /* Salt */ 21 + 22 + /* Text colors */ 23 + --ifm-font-color-base: #201F26; /* Pepper */ 24 + --ifm-font-color-secondary: #605F6B; /* Oyster */ 25 + 26 + /* Code blocks */ 27 + --ifm-code-background: #F1EFEF; /* Salt */ 16 28 --ifm-code-font-size: 95%; 17 - --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 29 + 30 + --docusaurus-highlighted-code-line-bg: rgba(0, 164, 255, 0.1); 18 31 } 19 32 20 - /* For readability concerns, you should choose a lighter palette in dark mode. */ 21 33 [data-theme='dark'] { 22 - --ifm-color-primary: #25c2a0; 23 - --ifm-color-primary-dark: #21af90; 24 - --ifm-color-primary-darker: #1fa588; 25 - --ifm-color-primary-darkest: #1a8870; 26 - --ifm-color-primary-light: #29d5b0; 27 - --ifm-color-primary-lighter: #32d8b4; 28 - --ifm-color-primary-lightest: #4fddbf; 29 - --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 34 + /* Noteleaf Color Palette - Dark Mode */ 35 + --ifm-color-primary: #00A4FF; /* Malibu - Blue primary */ 36 + --ifm-color-primary-dark: #0094e6; 37 + --ifm-color-primary-darker: #008ad9; 38 + --ifm-color-primary-darkest: #0072b3; 39 + --ifm-color-primary-light: #1aadff; 40 + --ifm-color-primary-lighter: #33b7ff; 41 + --ifm-color-primary-lightest: #66c9ff; 42 + 43 + --ifm-color-success: #00FFB2; /* Julep - Green */ 44 + --ifm-color-info: #5CDFEA; /* Lichen - Teal */ 45 + --ifm-color-warning: #FF985A; /* Tang - Orange */ 46 + --ifm-color-danger: #EB4268; /* Sriracha - Red */ 47 + 48 + /* Background colors - Dark mode */ 49 + --ifm-background-color: #201F26; /* Pepper - Darkest base */ 50 + --ifm-background-surface-color: #2d2c35; /* BBQ - Secondary background */ 51 + 52 + /* Card/elevated backgrounds */ 53 + --ifm-card-background-color: #3A3943; /* Charcoal - Tertiary */ 54 + 55 + /* Text colors */ 56 + --ifm-font-color-base: #F1EFEF; /* Salt - Primary text */ 57 + --ifm-font-color-secondary: #BFBCC8; /* Smoke - Secondary text */ 58 + --ifm-font-color-base-inverse: #201F26; /* Pepper */ 59 + 60 + /* Headings */ 61 + --ifm-heading-color: #00A4FF; /* Malibu */ 62 + 63 + /* Code blocks */ 64 + --ifm-code-background: #2d2c35; /* BBQ */ 65 + --ifm-pre-background: #2d2c35; /* BBQ */ 66 + 67 + /* Borders */ 68 + --ifm-color-emphasis-300: #4D4C57; /* Iron */ 69 + --ifm-hr-background-color: #4D4C57; /* Iron */ 70 + 71 + /* Links */ 72 + --ifm-link-color: #5CDFEA; /* Lichen - Accent */ 73 + --ifm-link-hover-color: #00FFB2; /* Julep - Success */ 74 + 75 + --docusaurus-highlighted-code-line-bg: rgba(0, 164, 255, 0.15); 76 + } 77 + 78 + .footer { 79 + background: var(--ifm-background-color); 80 + color: var(--ifm-font-color-base); 30 81 }
+39 -15
website/src/pages/index.module.css
··· 1 - /** 2 - * CSS files with the .module.css suffix will be treated as CSS modules 3 - * and scoped locally. 4 - */ 5 - 6 1 .heroBanner { 7 - padding: 4rem 0; 8 - text-align: center; 9 - position: relative; 10 - overflow: hidden; 2 + padding: 4rem 0; 3 + text-align: center; 4 + position: relative; 5 + overflow: hidden; 6 + background: var(--ifm-background-color); 7 + color: var(--ifm-font-color-base); 8 + } 9 + 10 + .heroTitle { 11 + font-size: clamp(2.5rem, 6vw, 4.5rem); 12 + font-weight: 700; 13 + background: linear-gradient( 14 + 100deg, 15 + var(--ifm-color-primary-lightest), 16 + var(--ifm-color-success), 17 + var(--ifm-color-warning) 18 + ); 19 + -webkit-background-clip: text; 20 + -webkit-text-fill-color: transparent; 21 + text-shadow: 0 10px 30px rgba(0, 0, 0, 0.25); 22 + position: relative; 23 + z-index: 1; 24 + } 25 + 26 + .heroSubtitle { 27 + font-size: 1.4rem; 28 + max-width: 720px; 29 + margin: 1.5rem auto 0; 30 + color: var(--ifm-font-color-secondary); 31 + font-weight: 500; 32 + position: relative; 33 + z-index: 1; 11 34 } 12 35 13 36 @media screen and (max-width: 996px) { 14 - .heroBanner { 15 - padding: 2rem; 16 - } 37 + .heroBanner { 38 + padding: 2rem; 39 + } 17 40 } 18 41 19 42 .buttons { 20 - display: flex; 21 - align-items: center; 22 - justify-content: center; 43 + margin-top: 24px; 44 + display: flex; 45 + align-items: center; 46 + justify-content: center; 23 47 }
+16 -22
website/src/pages/index.tsx
··· 1 - import type {ReactNode} from 'react'; 2 - import clsx from 'clsx'; 3 - import Link from '@docusaurus/Link'; 4 - import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 5 - import Layout from '@theme/Layout'; 6 - import HomepageFeatures from '@site/src/components/HomepageFeatures'; 7 - import Heading from '@theme/Heading'; 1 + import Link from "@docusaurus/Link"; 2 + import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; 3 + import HomepageFeatures from "@site/src/components/HomepageFeatures"; 4 + import Heading from "@theme/Heading"; 5 + import Layout from "@theme/Layout"; 6 + import clsx from "clsx"; 7 + import type { ReactNode } from "react"; 8 8 9 - import styles from './index.module.css'; 9 + import styles from "./index.module.css"; 10 10 11 11 function HomepageHeader() { 12 - const {siteConfig} = useDocusaurusContext(); 12 + const { siteConfig } = useDocusaurusContext(); 13 13 return ( 14 - <header className={clsx('hero hero--primary', styles.heroBanner)}> 14 + <header className={clsx("hero", styles.heroBanner)}> 15 15 <div className="container"> 16 - <Heading as="h1" className="hero__title"> 17 - {siteConfig.title} 18 - </Heading> 19 - <p className="hero__subtitle">{siteConfig.tagline}</p> 16 + <Heading as="h1" className={clsx("hero__title", styles.heroTitle)}>{siteConfig.title}</Heading> 17 + <p className={clsx("hero__subtitle", styles.heroSubtitle)}>{siteConfig.tagline}</p> 20 18 <div className={styles.buttons}> 21 - <Link 22 - className="button button--secondary button--lg" 23 - to="/docs/intro"> 24 - Docusaurus Tutorial - 5min โฑ๏ธ 25 - </Link> 19 + <Link className="button button--info button--lg" to="/docs/quickstart">Get Started</Link> 26 20 </div> 27 21 </div> 28 22 </header> ··· 30 24 } 31 25 32 26 export default function Home(): ReactNode { 33 - const {siteConfig} = useDocusaurusContext(); 27 + const { siteConfig } = useDocusaurusContext(); 34 28 return ( 35 29 <Layout 36 - title={`Hello from ${siteConfig.title}`} 37 - description="Description will go into a meta tag in <head />"> 30 + title="Terminal-based Personal Information Manager" 31 + description="Manage tasks, notes, articles, and media from your terminal with Noteleaf"> 38 32 <HomepageHeader /> 39 33 <main> 40 34 <HomepageFeatures />
-7
website/src/pages/markdown-page.md
··· 1 - --- 2 - title: Markdown page example 3 - --- 4 - 5 - # Markdown page example 6 - 7 - You don't need React to write simple standalone pages.
website/static/img/docusaurus-social-card.jpg

This is a binary file and will not be displayed.

website/static/img/docusaurus.png

This is a binary file and will not be displayed.

website/static/img/favicon.ico

This is a binary file and will not be displayed.

+12 -1
website/static/img/logo.svg
··· 1 - <svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#FFF" d="M99 52h84v34H99z"/><path d="M23 163c-7.398 0-13.843-4.027-17.303-10A19.886 19.886 0 0 0 3 163c0 11.046 8.954 20 20 20h20v-20H23z" fill="#3ECC5F"/><path d="M112.98 57.376L183 53V43c0-11.046-8.954-20-20-20H73l-2.5-4.33c-1.112-1.925-3.889-1.925-5 0L63 23l-2.5-4.33c-1.111-1.925-3.889-1.925-5 0L53 23l-2.5-4.33c-1.111-1.925-3.889-1.925-5 0L43 23c-.022 0-.042.003-.065.003l-4.142-4.141c-1.57-1.571-4.252-.853-4.828 1.294l-1.369 5.104-5.192-1.392c-2.148-.575-4.111 1.389-3.535 3.536l1.39 5.193-5.102 1.367c-2.148.576-2.867 3.259-1.296 4.83l4.142 4.142c0 .021-.003.042-.003.064l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 53l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 63l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 73l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 83l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 93l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 103l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 113l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 123l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 133l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 143l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 153l-4.33 2.5c-1.925 1.111-1.925 3.889 0 5L23 163c0 11.046 8.954 20 20 20h120c11.046 0 20-8.954 20-20V83l-70.02-4.376A10.645 10.645 0 0 1 103 68c0-5.621 4.37-10.273 9.98-10.624" fill="#3ECC5F"/><path fill="#3ECC5F" d="M143 183h30v-40h-30z"/><path d="M193 158c-.219 0-.428.037-.639.064-.038-.15-.074-.301-.116-.451A5 5 0 0 0 190.32 148a4.96 4.96 0 0 0-3.016 1.036 26.531 26.531 0 0 0-.335-.336 4.955 4.955 0 0 0 1.011-2.987 5 5 0 0 0-9.599-1.959c-.148-.042-.297-.077-.445-.115.027-.211.064-.42.064-.639a5 5 0 0 0-5-5 5 5 0 0 0-5 5c0 .219.037.428.064.639-.148.038-.297.073-.445.115a4.998 4.998 0 0 0-9.599 1.959c0 1.125.384 2.151 1.011 2.987-3.717 3.632-6.031 8.693-6.031 14.3 0 11.046 8.954 20 20 20 9.339 0 17.16-6.41 19.361-15.064.211.027.42.064.639.064a5 5 0 0 0 5-5 5 5 0 0 0-5-5" fill="#44D860"/><path fill="#3ECC5F" d="M153 123h30v-20h-30z"/><path d="M193 115.5a2.5 2.5 0 1 0 0-5c-.109 0-.214.019-.319.032-.02-.075-.037-.15-.058-.225a2.501 2.501 0 0 0-.963-4.807c-.569 0-1.088.197-1.508.518a6.653 6.653 0 0 0-.168-.168c.314-.417.506-.931.506-1.494a2.5 2.5 0 0 0-4.8-.979A9.987 9.987 0 0 0 183 103c-5.522 0-10 4.478-10 10s4.478 10 10 10c.934 0 1.833-.138 2.69-.377a2.5 2.5 0 0 0 4.8-.979c0-.563-.192-1.077-.506-1.494.057-.055.113-.111.168-.168.42.321.939.518 1.508.518a2.5 2.5 0 0 0 .963-4.807c.021-.074.038-.15.058-.225.105.013.21.032.319.032" fill="#44D860"/><path d="M63 55.5a2.5 2.5 0 0 1-2.5-2.5c0-4.136-3.364-7.5-7.5-7.5s-7.5 3.364-7.5 7.5a2.5 2.5 0 1 1-5 0c0-6.893 5.607-12.5 12.5-12.5S65.5 46.107 65.5 53a2.5 2.5 0 0 1-2.5 2.5" fill="#000"/><path d="M103 183h60c11.046 0 20-8.954 20-20V93h-60c-11.046 0-20 8.954-20 20v70z" fill="#FFFF50"/><path d="M168.02 124h-50.04a1 1 0 1 1 0-2h50.04a1 1 0 1 1 0 2m0 20h-50.04a1 1 0 1 1 0-2h50.04a1 1 0 1 1 0 2m0 20h-50.04a1 1 0 1 1 0-2h50.04a1 1 0 1 1 0 2m0-49.814h-50.04a1 1 0 1 1 0-2h50.04a1 1 0 1 1 0 2m0 19.814h-50.04a1 1 0 1 1 0-2h50.04a1 1 0 1 1 0 2m0 20h-50.04a1 1 0 1 1 0-2h50.04a1 1 0 1 1 0 2M183 61.611c-.012 0-.022-.006-.034-.005-3.09.105-4.552 3.196-5.842 5.923-1.346 2.85-2.387 4.703-4.093 4.647-1.889-.068-2.969-2.202-4.113-4.46-1.314-2.594-2.814-5.536-5.963-5.426-3.046.104-4.513 2.794-5.807 5.167-1.377 2.528-2.314 4.065-4.121 3.994-1.927-.07-2.951-1.805-4.136-3.813-1.321-2.236-2.848-4.75-5.936-4.664-2.994.103-4.465 2.385-5.763 4.4-1.373 2.13-2.335 3.428-4.165 3.351-1.973-.07-2.992-1.51-4.171-3.177-1.324-1.873-2.816-3.993-5.895-3.89-2.928.1-4.399 1.97-5.696 3.618-1.232 1.564-2.194 2.802-4.229 2.724a1 1 0 0 0-.072 2c3.017.101 4.545-1.8 5.872-3.487 1.177-1.496 2.193-2.787 4.193-2.855 1.926-.082 2.829 1.115 4.195 3.045 1.297 1.834 2.769 3.914 5.731 4.021 3.103.104 4.596-2.215 5.918-4.267 1.182-1.834 2.202-3.417 4.15-3.484 1.793-.067 2.769 1.35 4.145 3.681 1.297 2.197 2.766 4.686 5.787 4.796 3.125.108 4.634-2.62 5.949-5.035 1.139-2.088 2.214-4.06 4.119-4.126 1.793-.042 2.728 1.595 4.111 4.33 1.292 2.553 2.757 5.445 5.825 5.556l.169.003c3.064 0 4.518-3.075 5.805-5.794 1.139-2.41 2.217-4.68 4.067-4.773v-2z" fill="#000"/><path fill="#3ECC5F" d="M83 183h40v-40H83z"/><path d="M143 158c-.219 0-.428.037-.639.064-.038-.15-.074-.301-.116-.451A5 5 0 0 0 140.32 148a4.96 4.96 0 0 0-3.016 1.036 26.531 26.531 0 0 0-.335-.336 4.955 4.955 0 0 0 1.011-2.987 5 5 0 0 0-9.599-1.959c-.148-.042-.297-.077-.445-.115.027-.211.064-.42.064-.639a5 5 0 0 0-5-5 5 5 0 0 0-5 5c0 .219.037.428.064.639-.148.038-.297.073-.445.115a4.998 4.998 0 0 0-9.599 1.959c0 1.125.384 2.151 1.011 2.987-3.717 3.632-6.031 8.693-6.031 14.3 0 11.046 8.954 20 20 20 9.339 0 17.16-6.41 19.361-15.064.211.027.42.064.639.064a5 5 0 0 0 5-5 5 5 0 0 0-5-5" fill="#44D860"/><path fill="#3ECC5F" d="M83 123h40v-20H83z"/><path d="M133 115.5a2.5 2.5 0 1 0 0-5c-.109 0-.214.019-.319.032-.02-.075-.037-.15-.058-.225a2.501 2.501 0 0 0-.963-4.807c-.569 0-1.088.197-1.508.518a6.653 6.653 0 0 0-.168-.168c.314-.417.506-.931.506-1.494a2.5 2.5 0 0 0-4.8-.979A9.987 9.987 0 0 0 123 103c-5.522 0-10 4.478-10 10s4.478 10 10 10c.934 0 1.833-.138 2.69-.377a2.5 2.5 0 0 0 4.8-.979c0-.563-.192-1.077-.506-1.494.057-.055.113-.111.168-.168.42.321.939.518 1.508.518a2.5 2.5 0 0 0 .963-4.807c.021-.074.038-.15.058-.225.105.013.21.032.319.032" fill="#44D860"/><path d="M143 41.75c-.16 0-.33-.02-.49-.05a2.52 2.52 0 0 1-.47-.14c-.15-.06-.29-.14-.431-.23-.13-.09-.259-.2-.38-.31-.109-.12-.219-.24-.309-.38s-.17-.28-.231-.43a2.619 2.619 0 0 1-.189-.96c0-.16.02-.33.05-.49.03-.16.08-.31.139-.47.061-.15.141-.29.231-.43.09-.13.2-.26.309-.38.121-.11.25-.22.38-.31.141-.09.281-.17.431-.23.149-.06.31-.11.47-.14.32-.07.65-.07.98 0 .159.03.32.08.47.14.149.06.29.14.43.23.13.09.259.2.38.31.11.12.22.25.31.38.09.14.17.28.23.43.06.16.11.31.14.47.029.16.05.33.05.49 0 .66-.271 1.31-.73 1.77-.121.11-.25.22-.38.31-.14.09-.281.17-.43.23a2.565 2.565 0 0 1-.96.19m20-1.25c-.66 0-1.3-.27-1.771-.73a3.802 3.802 0 0 1-.309-.38c-.09-.14-.17-.28-.231-.43a2.619 2.619 0 0 1-.189-.96c0-.66.27-1.3.729-1.77.121-.11.25-.22.38-.31.141-.09.281-.17.431-.23.149-.06.31-.11.47-.14.32-.07.66-.07.98 0 .159.03.32.08.47.14.149.06.29.14.43.23.13.09.259.2.38.31.459.47.73 1.11.73 1.77 0 .16-.021.33-.05.49-.03.16-.08.32-.14.47-.07.15-.14.29-.23.43-.09.13-.2.26-.31.38-.121.11-.25.22-.38.31-.14.09-.281.17-.43.23a2.565 2.565 0 0 1-.96.19" fill="#000"/></g></svg> 1 + <svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 512 512"> 2 + <defs> 3 + <linearGradient id="leafGradient" x1="0%" y1="0%" x2="100%" y2="100%"> 4 + <stop offset="0%" style="stop-color:#00A4FF;stop-opacity:1" /> 5 + <stop offset="100%" style="stop-color:#00A4FF;stop-opacity:1" /> 6 + </linearGradient> 7 + </defs> 8 + <path fill="none" stroke="url(#leafGradient)" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" 9 + d="M321.89 171.42C233 114 141 155.22 56 65.22c-19.8-21-8.3 235.5 98.1 332.7c77.79 71 197.9 63.08 238.4-5.92s18.28-163.17-70.61-220.58" /> 10 + <path fill="none" stroke="#00A4FF" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" 11 + d="M173 253c86 81 175 129 292 147" /> 12 + </svg>
website/static/img/social-card.png

This is a binary file and will not be displayed.

-171
website/static/img/undraw_docusaurus_mountain.svg
··· 1 - <svg xmlns="http://www.w3.org/2000/svg" width="1088" height="687.962" viewBox="0 0 1088 687.962"> 2 - <title>Easy to Use</title> 3 - <g id="Group_12" data-name="Group 12" transform="translate(-57 -56)"> 4 - <g id="Group_11" data-name="Group 11" transform="translate(57 56)"> 5 - <path id="Path_83" data-name="Path 83" d="M1017.81,560.461c-5.27,45.15-16.22,81.4-31.25,110.31-20,38.52-54.21,54.04-84.77,70.28a193.275,193.275,0,0,1-27.46,11.94c-55.61,19.3-117.85,14.18-166.74,3.99a657.282,657.282,0,0,0-104.09-13.16q-14.97-.675-29.97-.67c-15.42.02-293.07,5.29-360.67-131.57-16.69-33.76-28.13-75-32.24-125.27-11.63-142.12,52.29-235.46,134.74-296.47,155.97-115.41,369.76-110.57,523.43,7.88C941.15,276.621,1036.99,396.031,1017.81,560.461Z" transform="translate(-56 -106.019)" fill="#3f3d56"/> 6 - <path id="Path_84" data-name="Path 84" d="M986.56,670.771c-20,38.52-47.21,64.04-77.77,80.28a193.272,193.272,0,0,1-27.46,11.94c-55.61,19.3-117.85,14.18-166.74,3.99a657.3,657.3,0,0,0-104.09-13.16q-14.97-.675-29.97-.67-23.13.03-46.25,1.72c-100.17,7.36-253.82-6.43-321.42-143.29L382,283.981,444.95,445.6l20.09,51.59,55.37-75.98L549,381.981l130.2,149.27,36.8-81.27L970.78,657.9l14.21,11.59Z" transform="translate(-56 -106.019)" fill="#f2f2f2"/> 7 - <path id="Path_85" data-name="Path 85" d="M302,282.962l26-57,36,83-31-60Z" opacity="0.1"/> 8 - <path id="Path_86" data-name="Path 86" d="M610.5,753.821q-14.97-.675-29.97-.67L465.04,497.191Z" transform="translate(-56 -106.019)" opacity="0.1"/> 9 - <path id="Path_87" data-name="Path 87" d="M464.411,315.191,493,292.962l130,150-132-128Z" opacity="0.1"/> 10 - <path id="Path_88" data-name="Path 88" d="M908.79,751.051a193.265,193.265,0,0,1-27.46,11.94L679.2,531.251Z" transform="translate(-56 -106.019)" opacity="0.1"/> 11 - <circle id="Ellipse_11" data-name="Ellipse 11" cx="3" cy="3" r="3" transform="translate(479 98.962)" fill="#f2f2f2"/> 12 - <circle id="Ellipse_12" data-name="Ellipse 12" cx="3" cy="3" r="3" transform="translate(396 201.962)" fill="#f2f2f2"/> 13 - <circle id="Ellipse_13" data-name="Ellipse 13" cx="2" cy="2" r="2" transform="translate(600 220.962)" fill="#f2f2f2"/> 14 - <circle id="Ellipse_14" data-name="Ellipse 14" cx="2" cy="2" r="2" transform="translate(180 265.962)" fill="#f2f2f2"/> 15 - <circle id="Ellipse_15" data-name="Ellipse 15" cx="2" cy="2" r="2" transform="translate(612 96.962)" fill="#f2f2f2"/> 16 - <circle id="Ellipse_16" data-name="Ellipse 16" cx="2" cy="2" r="2" transform="translate(736 192.962)" fill="#f2f2f2"/> 17 - <circle id="Ellipse_17" data-name="Ellipse 17" cx="2" cy="2" r="2" transform="translate(858 344.962)" fill="#f2f2f2"/> 18 - <path id="Path_89" data-name="Path 89" d="M306,121.222h-2.76v-2.76h-1.48v2.76H299V122.7h2.76v2.759h1.48V122.7H306Z" fill="#f2f2f2"/> 19 - <path id="Path_90" data-name="Path 90" d="M848,424.222h-2.76v-2.76h-1.48v2.76H841V425.7h2.76v2.759h1.48V425.7H848Z" fill="#f2f2f2"/> 20 - <path id="Path_91" data-name="Path 91" d="M1144,719.981c0,16.569-243.557,74-544,74s-544-57.431-544-74,243.557,14,544,14S1144,703.413,1144,719.981Z" transform="translate(-56 -106.019)" fill="#3f3d56"/> 21 - <path id="Path_92" data-name="Path 92" d="M1144,719.981c0,16.569-243.557,74-544,74s-544-57.431-544-74,243.557,14,544,14S1144,703.413,1144,719.981Z" transform="translate(-56 -106.019)" opacity="0.1"/> 22 - <ellipse id="Ellipse_18" data-name="Ellipse 18" cx="544" cy="30" rx="544" ry="30" transform="translate(0 583.962)" fill="#3f3d56"/> 23 - <path id="Path_93" data-name="Path 93" d="M624,677.981c0,33.137-14.775,24-33,24s-33,9.137-33-24,33-96,33-96S624,644.844,624,677.981Z" transform="translate(-56 -106.019)" fill="#ff6584"/> 24 - <path id="Path_94" data-name="Path 94" d="M606,690.66c0,15.062-6.716,10.909-15,10.909s-15,4.153-15-10.909,15-43.636,15-43.636S606,675.6,606,690.66Z" transform="translate(-56 -106.019)" opacity="0.1"/> 25 - <rect id="Rectangle_97" data-name="Rectangle 97" width="92" height="18" rx="9" transform="translate(489 604.962)" fill="#2f2e41"/> 26 - <rect id="Rectangle_98" data-name="Rectangle 98" width="92" height="18" rx="9" transform="translate(489 586.962)" fill="#2f2e41"/> 27 - <path id="Path_95" data-name="Path 95" d="M193,596.547c0,55.343,34.719,100.126,77.626,100.126" transform="translate(-56 -106.019)" fill="#3f3d56"/> 28 - <path id="Path_96" data-name="Path 96" d="M270.626,696.673c0-55.965,38.745-101.251,86.626-101.251" transform="translate(-56 -106.019)" fill="#6c63ff"/> 29 - <path id="Path_97" data-name="Path 97" d="M221.125,601.564c0,52.57,22.14,95.109,49.5,95.109" transform="translate(-56 -106.019)" fill="#6c63ff"/> 30 - <path id="Path_98" data-name="Path 98" d="M270.626,696.673c0-71.511,44.783-129.377,100.126-129.377" transform="translate(-56 -106.019)" fill="#3f3d56"/> 31 - <path id="Path_99" data-name="Path 99" d="M254.3,697.379s11.009-.339,14.326-2.7,16.934-5.183,17.757-1.395,16.544,18.844,4.115,18.945-28.879-1.936-32.19-3.953S254.3,697.379,254.3,697.379Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/> 32 - <path id="Path_100" data-name="Path 100" d="M290.716,710.909c-12.429.1-28.879-1.936-32.19-3.953-2.522-1.536-3.527-7.048-3.863-9.591l-.368.014s.7,8.879,4.009,10.9,19.761,4.053,32.19,3.953c3.588-.029,4.827-1.305,4.759-3.2C294.755,710.174,293.386,710.887,290.716,710.909Z" transform="translate(-56 -106.019)" opacity="0.2"/> 33 - <path id="Path_101" data-name="Path 101" d="M777.429,633.081c0,38.029,23.857,68.8,53.341,68.8" transform="translate(-56 -106.019)" fill="#3f3d56"/> 34 - <path id="Path_102" data-name="Path 102" d="M830.769,701.882c0-38.456,26.623-69.575,59.525-69.575" transform="translate(-56 -106.019)" fill="#6c63ff"/> 35 - <path id="Path_103" data-name="Path 103" d="M796.755,636.528c0,36.124,15.213,65.354,34.014,65.354" transform="translate(-56 -106.019)" fill="#6c63ff"/> 36 - <path id="Path_104" data-name="Path 104" d="M830.769,701.882c0-49.139,30.773-88.9,68.8-88.9" transform="translate(-56 -106.019)" fill="#3f3d56"/> 37 - <path id="Path_105" data-name="Path 105" d="M819.548,702.367s7.565-.233,9.844-1.856,11.636-3.562,12.2-.958,11.368,12.949,2.828,13.018-19.844-1.33-22.119-2.716S819.548,702.367,819.548,702.367Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/> 38 - <path id="Path_106" data-name="Path 106" d="M844.574,711.664c-8.54.069-19.844-1.33-22.119-2.716-1.733-1.056-2.423-4.843-2.654-6.59l-.253.01s.479,6.1,2.755,7.487,13.579,2.785,22.119,2.716c2.465-.02,3.317-.9,3.27-2.2C847.349,711.159,846.409,711.649,844.574,711.664Z" transform="translate(-56 -106.019)" opacity="0.2"/> 39 - <path id="Path_107" data-name="Path 107" d="M949.813,724.718s11.36-1.729,14.5-4.591,16.89-7.488,18.217-3.667,19.494,17.447,6.633,19.107-30.153,1.609-33.835-.065S949.813,724.718,949.813,724.718Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/> 40 - <path id="Path_108" data-name="Path 108" d="M989.228,734.173c-12.86,1.659-30.153,1.609-33.835-.065-2.8-1.275-4.535-6.858-5.2-9.45l-.379.061s1.833,9.109,5.516,10.783,20.975,1.725,33.835.065c3.712-.479,4.836-1.956,4.529-3.906C993.319,732.907,991.991,733.817,989.228,734.173Z" transform="translate(-56 -106.019)" opacity="0.2"/> 41 - <path id="Path_109" data-name="Path 109" d="M670.26,723.9s9.587-1.459,12.237-3.875,14.255-6.32,15.374-3.095,16.452,14.725,5.6,16.125-25.448,1.358-28.555-.055S670.26,723.9,670.26,723.9Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/> 42 - <path id="Path_110" data-name="Path 110" d="M703.524,731.875c-10.853,1.4-25.448,1.358-28.555-.055-2.367-1.076-3.827-5.788-4.39-7.976l-.32.051s1.547,7.687,4.655,9.1,17.7,1.456,28.555.055c3.133-.4,4.081-1.651,3.822-3.3C706.977,730.807,705.856,731.575,703.524,731.875Z" transform="translate(-56 -106.019)" opacity="0.2"/> 43 - <path id="Path_111" data-name="Path 111" d="M178.389,719.109s7.463-1.136,9.527-3.016,11.1-4.92,11.969-2.409,12.808,11.463,4.358,12.553-19.811,1.057-22.23-.043S178.389,719.109,178.389,719.109Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/> 44 - <path id="Path_112" data-name="Path 112" d="M204.285,725.321c-8.449,1.09-19.811,1.057-22.23-.043-1.842-.838-2.979-4.506-3.417-6.209l-.249.04s1.2,5.984,3.624,7.085,13.781,1.133,22.23.043c2.439-.315,3.177-1.285,2.976-2.566C206.973,724.489,206.1,725.087,204.285,725.321Z" transform="translate(-56 -106.019)" opacity="0.2"/> 45 - <path id="Path_113" data-name="Path 113" d="M439.7,707.337c0,30.22-42.124,20.873-93.7,20.873s-93.074,9.347-93.074-20.873,42.118-36.793,93.694-36.793S439.7,677.117,439.7,707.337Z" transform="translate(-56 -106.019)" opacity="0.1"/> 46 - <path id="Path_114" data-name="Path 114" d="M439.7,699.9c0,30.22-42.124,20.873-93.7,20.873s-93.074,9.347-93.074-20.873S295.04,663.1,346.616,663.1,439.7,669.676,439.7,699.9Z" transform="translate(-56 -106.019)" fill="#3f3d56"/> 47 - </g> 48 - <g id="docusaurus_keytar" transform="translate(312.271 493.733)"> 49 - <path id="Path_40" data-name="Path 40" d="M99,52h91.791V89.153H99Z" transform="translate(5.904 -14.001)" fill="#fff" fill-rule="evenodd"/> 50 - <path id="Path_41" data-name="Path 41" d="M24.855,163.927A21.828,21.828,0,0,1,5.947,153a21.829,21.829,0,0,0,18.908,32.782H46.71V163.927Z" transform="translate(-3 -4.634)" fill="#3ecc5f" fill-rule="evenodd"/> 51 - <path id="Path_42" data-name="Path 42" d="M121.861,61.1l76.514-4.782V45.39A21.854,21.854,0,0,0,176.52,23.535H78.173L75.441,18.8a3.154,3.154,0,0,0-5.464,0l-2.732,4.732L64.513,18.8a3.154,3.154,0,0,0-5.464,0l-2.732,4.732L53.586,18.8a3.154,3.154,0,0,0-5.464,0L45.39,23.535c-.024,0-.046,0-.071,0l-4.526-4.525a3.153,3.153,0,0,0-5.276,1.414l-1.5,5.577-5.674-1.521a3.154,3.154,0,0,0-3.863,3.864L26,34.023l-5.575,1.494a3.155,3.155,0,0,0-1.416,5.278l4.526,4.526c0,.023,0,.046,0,.07L18.8,48.122a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,59.05a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,69.977a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,80.9a3.154,3.154,0,0,0,0,5.464L23.535,89.1,18.8,91.832a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,102.76a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,113.687a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,124.615a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,135.542a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,146.469a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,157.4a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,168.324a3.154,3.154,0,0,0,0,5.464l4.732,2.732A21.854,21.854,0,0,0,45.39,198.375H176.52a21.854,21.854,0,0,0,21.855-21.855V89.1l-76.514-4.782a11.632,11.632,0,0,1,0-23.219" transform="translate(-1.681 -17.226)" fill="#3ecc5f" fill-rule="evenodd"/> 52 - <path id="Path_43" data-name="Path 43" d="M143,186.71h32.782V143H143Z" transform="translate(9.984 -5.561)" fill="#3ecc5f" fill-rule="evenodd"/> 53 - <path id="Path_44" data-name="Path 44" d="M196.71,159.855a5.438,5.438,0,0,0-.7.07c-.042-.164-.081-.329-.127-.493a5.457,5.457,0,1,0-5.4-9.372q-.181-.185-.366-.367a5.454,5.454,0,1,0-9.384-5.4c-.162-.046-.325-.084-.486-.126a5.467,5.467,0,1,0-10.788,0c-.162.042-.325.08-.486.126a5.457,5.457,0,1,0-9.384,5.4,21.843,21.843,0,1,0,36.421,21.02,5.452,5.452,0,1,0,.7-10.858" transform="translate(10.912 -6.025)" fill="#44d860" fill-rule="evenodd"/> 54 - <path id="Path_45" data-name="Path 45" d="M153,124.855h32.782V103H153Z" transform="translate(10.912 -9.271)" fill="#3ecc5f" fill-rule="evenodd"/> 55 - <path id="Path_46" data-name="Path 46" d="M194.855,116.765a2.732,2.732,0,1,0,0-5.464,2.811,2.811,0,0,0-.349.035c-.022-.082-.04-.164-.063-.246a2.733,2.733,0,0,0-1.052-5.253,2.7,2.7,0,0,0-1.648.566q-.09-.093-.184-.184a2.7,2.7,0,0,0,.553-1.633,2.732,2.732,0,0,0-5.245-1.07,10.928,10.928,0,1,0,0,21.031,2.732,2.732,0,0,0,5.245-1.07,2.7,2.7,0,0,0-.553-1.633q.093-.09.184-.184a2.7,2.7,0,0,0,1.648.566,2.732,2.732,0,0,0,1.052-5.253c.023-.081.042-.164.063-.246a2.814,2.814,0,0,0,.349.035" transform="translate(12.767 -9.377)" fill="#44d860" fill-rule="evenodd"/> 56 - <path id="Path_47" data-name="Path 47" d="M65.087,56.891a2.732,2.732,0,0,1-2.732-2.732,8.2,8.2,0,0,0-16.391,0,2.732,2.732,0,0,1-5.464,0,13.659,13.659,0,0,1,27.319,0,2.732,2.732,0,0,1-2.732,2.732" transform="translate(0.478 -15.068)" fill-rule="evenodd"/> 57 - <path id="Path_48" data-name="Path 48" d="M103,191.347h65.565a21.854,21.854,0,0,0,21.855-21.855V93H124.855A21.854,21.854,0,0,0,103,114.855Z" transform="translate(6.275 -10.199)" fill="#ffff50" fill-rule="evenodd"/> 58 - <path id="Path_49" data-name="Path 49" d="M173.216,129.787H118.535a1.093,1.093,0,1,1,0-2.185h54.681a1.093,1.093,0,0,1,0,2.185m0,21.855H118.535a1.093,1.093,0,1,1,0-2.186h54.681a1.093,1.093,0,0,1,0,2.186m0,21.855H118.535a1.093,1.093,0,1,1,0-2.185h54.681a1.093,1.093,0,0,1,0,2.185m0-54.434H118.535a1.093,1.093,0,1,1,0-2.185h54.681a1.093,1.093,0,0,1,0,2.185m0,21.652H118.535a1.093,1.093,0,1,1,0-2.186h54.681a1.093,1.093,0,0,1,0,2.186m0,21.855H118.535a1.093,1.093,0,1,1,0-2.186h54.681a1.093,1.093,0,0,1,0,2.186M189.585,61.611c-.013,0-.024-.007-.037-.005-3.377.115-4.974,3.492-6.384,6.472-1.471,3.114-2.608,5.139-4.473,5.078-2.064-.074-3.244-2.406-4.494-4.874-1.436-2.835-3.075-6.049-6.516-5.929-3.329.114-4.932,3.053-6.346,5.646-1.5,2.762-2.529,4.442-4.5,4.364-2.106-.076-3.225-1.972-4.52-4.167-1.444-2.443-3.112-5.191-6.487-5.1-3.272.113-4.879,2.606-6.3,4.808-1.5,2.328-2.552,3.746-4.551,3.662-2.156-.076-3.27-1.65-4.558-3.472-1.447-2.047-3.077-4.363-6.442-4.251-3.2.109-4.807,2.153-6.224,3.954-1.346,1.709-2.4,3.062-4.621,2.977a1.093,1.093,0,0,0-.079,2.186c3.3.11,4.967-1.967,6.417-3.81,1.286-1.635,2.4-3.045,4.582-3.12,2.1-.09,3.091,1.218,4.584,3.327,1.417,2,3.026,4.277,6.263,4.394,3.391.114,5.022-2.42,6.467-4.663,1.292-2,2.406-3.734,4.535-3.807,1.959-.073,3.026,1.475,4.529,4.022,1.417,2.4,3.023,5.121,6.324,5.241,3.415.118,5.064-2.863,6.5-5.5,1.245-2.282,2.419-4.437,4.5-4.509,1.959-.046,2.981,1.743,4.492,4.732,1.412,2.79,3.013,5.95,6.365,6.071l.185,0c3.348,0,4.937-3.36,6.343-6.331,1.245-2.634,2.423-5.114,4.444-5.216Z" transform="translate(7.109 -13.11)" fill-rule="evenodd"/> 59 - <path id="Path_50" data-name="Path 50" d="M83,186.71h43.71V143H83Z" transform="translate(4.42 -5.561)" fill="#3ecc5f" fill-rule="evenodd"/> 60 - <g id="Group_8" data-name="Group 8" transform="matrix(0.966, -0.259, 0.259, 0.966, 109.327, 91.085)"> 61 - <rect id="Rectangle_3" data-name="Rectangle 3" width="92.361" height="36.462" rx="2" transform="translate(0 0)" fill="#d8d8d8"/> 62 - <g id="Group_2" data-name="Group 2" transform="translate(1.531 23.03)"> 63 - <rect id="Rectangle_4" data-name="Rectangle 4" width="5.336" height="5.336" rx="1" transform="translate(16.797 0)" fill="#4a4a4a"/> 64 - <rect id="Rectangle_5" data-name="Rectangle 5" width="5.336" height="5.336" rx="1" transform="translate(23.12 0)" fill="#4a4a4a"/> 65 - <rect id="Rectangle_6" data-name="Rectangle 6" width="5.336" height="5.336" rx="1" transform="translate(29.444 0)" fill="#4a4a4a"/> 66 - <rect id="Rectangle_7" data-name="Rectangle 7" width="5.336" height="5.336" rx="1" transform="translate(35.768 0)" fill="#4a4a4a"/> 67 - <rect id="Rectangle_8" data-name="Rectangle 8" width="5.336" height="5.336" rx="1" transform="translate(42.091 0)" fill="#4a4a4a"/> 68 - <rect id="Rectangle_9" data-name="Rectangle 9" width="5.336" height="5.336" rx="1" transform="translate(48.415 0)" fill="#4a4a4a"/> 69 - <rect id="Rectangle_10" data-name="Rectangle 10" width="5.336" height="5.336" rx="1" transform="translate(54.739 0)" fill="#4a4a4a"/> 70 - <rect id="Rectangle_11" data-name="Rectangle 11" width="5.336" height="5.336" rx="1" transform="translate(61.063 0)" fill="#4a4a4a"/> 71 - <rect id="Rectangle_12" data-name="Rectangle 12" width="5.336" height="5.336" rx="1" transform="translate(67.386 0)" fill="#4a4a4a"/> 72 - <path id="Path_51" data-name="Path 51" d="M1.093,0H14.518a1.093,1.093,0,0,1,1.093,1.093V4.243a1.093,1.093,0,0,1-1.093,1.093H1.093A1.093,1.093,0,0,1,0,4.243V1.093A1.093,1.093,0,0,1,1.093,0ZM75,0H88.426a1.093,1.093,0,0,1,1.093,1.093V4.243a1.093,1.093,0,0,1-1.093,1.093H75a1.093,1.093,0,0,1-1.093-1.093V1.093A1.093,1.093,0,0,1,75,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/> 73 - </g> 74 - <g id="Group_3" data-name="Group 3" transform="translate(1.531 10.261)"> 75 - <path id="Path_52" data-name="Path 52" d="M1.093,0H6.218A1.093,1.093,0,0,1,7.31,1.093V4.242A1.093,1.093,0,0,1,6.218,5.335H1.093A1.093,1.093,0,0,1,0,4.242V1.093A1.093,1.093,0,0,1,1.093,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/> 76 - <rect id="Rectangle_13" data-name="Rectangle 13" width="5.336" height="5.336" rx="1" transform="translate(8.299 0)" fill="#4a4a4a"/> 77 - <rect id="Rectangle_14" data-name="Rectangle 14" width="5.336" height="5.336" rx="1" transform="translate(14.623 0)" fill="#4a4a4a"/> 78 - <rect id="Rectangle_15" data-name="Rectangle 15" width="5.336" height="5.336" rx="1" transform="translate(20.947 0)" fill="#4a4a4a"/> 79 - <rect id="Rectangle_16" data-name="Rectangle 16" width="5.336" height="5.336" rx="1" transform="translate(27.271 0)" fill="#4a4a4a"/> 80 - <rect id="Rectangle_17" data-name="Rectangle 17" width="5.336" height="5.336" rx="1" transform="translate(33.594 0)" fill="#4a4a4a"/> 81 - <rect id="Rectangle_18" data-name="Rectangle 18" width="5.336" height="5.336" rx="1" transform="translate(39.918 0)" fill="#4a4a4a"/> 82 - <rect id="Rectangle_19" data-name="Rectangle 19" width="5.336" height="5.336" rx="1" transform="translate(46.242 0)" fill="#4a4a4a"/> 83 - <rect id="Rectangle_20" data-name="Rectangle 20" width="5.336" height="5.336" rx="1" transform="translate(52.565 0)" fill="#4a4a4a"/> 84 - <rect id="Rectangle_21" data-name="Rectangle 21" width="5.336" height="5.336" rx="1" transform="translate(58.888 0)" fill="#4a4a4a"/> 85 - <rect id="Rectangle_22" data-name="Rectangle 22" width="5.336" height="5.336" rx="1" transform="translate(65.212 0)" fill="#4a4a4a"/> 86 - <rect id="Rectangle_23" data-name="Rectangle 23" width="5.336" height="5.336" rx="1" transform="translate(71.536 0)" fill="#4a4a4a"/> 87 - <rect id="Rectangle_24" data-name="Rectangle 24" width="5.336" height="5.336" rx="1" transform="translate(77.859 0)" fill="#4a4a4a"/> 88 - <rect id="Rectangle_25" data-name="Rectangle 25" width="5.336" height="5.336" rx="1" transform="translate(84.183 0)" fill="#4a4a4a"/> 89 - </g> 90 - <g id="Group_4" data-name="Group 4" transform="translate(91.05 9.546) rotate(180)"> 91 - <path id="Path_53" data-name="Path 53" d="M1.093,0H6.219A1.093,1.093,0,0,1,7.312,1.093v3.15A1.093,1.093,0,0,1,6.219,5.336H1.093A1.093,1.093,0,0,1,0,4.243V1.093A1.093,1.093,0,0,1,1.093,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/> 92 - <rect id="Rectangle_26" data-name="Rectangle 26" width="5.336" height="5.336" rx="1" transform="translate(8.299 0)" fill="#4a4a4a"/> 93 - <rect id="Rectangle_27" data-name="Rectangle 27" width="5.336" height="5.336" rx="1" transform="translate(14.623 0)" fill="#4a4a4a"/> 94 - <rect id="Rectangle_28" data-name="Rectangle 28" width="5.336" height="5.336" rx="1" transform="translate(20.947 0)" fill="#4a4a4a"/> 95 - <rect id="Rectangle_29" data-name="Rectangle 29" width="5.336" height="5.336" rx="1" transform="translate(27.271 0)" fill="#4a4a4a"/> 96 - <rect id="Rectangle_30" data-name="Rectangle 30" width="5.336" height="5.336" rx="1" transform="translate(33.594 0)" fill="#4a4a4a"/> 97 - <rect id="Rectangle_31" data-name="Rectangle 31" width="5.336" height="5.336" rx="1" transform="translate(39.918 0)" fill="#4a4a4a"/> 98 - <rect id="Rectangle_32" data-name="Rectangle 32" width="5.336" height="5.336" rx="1" transform="translate(46.242 0)" fill="#4a4a4a"/> 99 - <rect id="Rectangle_33" data-name="Rectangle 33" width="5.336" height="5.336" rx="1" transform="translate(52.565 0)" fill="#4a4a4a"/> 100 - <rect id="Rectangle_34" data-name="Rectangle 34" width="5.336" height="5.336" rx="1" transform="translate(58.889 0)" fill="#4a4a4a"/> 101 - <rect id="Rectangle_35" data-name="Rectangle 35" width="5.336" height="5.336" rx="1" transform="translate(65.213 0)" fill="#4a4a4a"/> 102 - <rect id="Rectangle_36" data-name="Rectangle 36" width="5.336" height="5.336" rx="1" transform="translate(71.537 0)" fill="#4a4a4a"/> 103 - <rect id="Rectangle_37" data-name="Rectangle 37" width="5.336" height="5.336" rx="1" transform="translate(77.86 0)" fill="#4a4a4a"/> 104 - <rect id="Rectangle_38" data-name="Rectangle 38" width="5.336" height="5.336" rx="1" transform="translate(84.183 0)" fill="#4a4a4a"/> 105 - <rect id="Rectangle_39" data-name="Rectangle 39" width="5.336" height="5.336" rx="1" transform="translate(8.299 0)" fill="#4a4a4a"/> 106 - <rect id="Rectangle_40" data-name="Rectangle 40" width="5.336" height="5.336" rx="1" transform="translate(14.623 0)" fill="#4a4a4a"/> 107 - <rect id="Rectangle_41" data-name="Rectangle 41" width="5.336" height="5.336" rx="1" transform="translate(20.947 0)" fill="#4a4a4a"/> 108 - <rect id="Rectangle_42" data-name="Rectangle 42" width="5.336" height="5.336" rx="1" transform="translate(27.271 0)" fill="#4a4a4a"/> 109 - <rect id="Rectangle_43" data-name="Rectangle 43" width="5.336" height="5.336" rx="1" transform="translate(33.594 0)" fill="#4a4a4a"/> 110 - <rect id="Rectangle_44" data-name="Rectangle 44" width="5.336" height="5.336" rx="1" transform="translate(39.918 0)" fill="#4a4a4a"/> 111 - <rect id="Rectangle_45" data-name="Rectangle 45" width="5.336" height="5.336" rx="1" transform="translate(46.242 0)" fill="#4a4a4a"/> 112 - <rect id="Rectangle_46" data-name="Rectangle 46" width="5.336" height="5.336" rx="1" transform="translate(52.565 0)" fill="#4a4a4a"/> 113 - <rect id="Rectangle_47" data-name="Rectangle 47" width="5.336" height="5.336" rx="1" transform="translate(58.889 0)" fill="#4a4a4a"/> 114 - <rect id="Rectangle_48" data-name="Rectangle 48" width="5.336" height="5.336" rx="1" transform="translate(65.213 0)" fill="#4a4a4a"/> 115 - <rect id="Rectangle_49" data-name="Rectangle 49" width="5.336" height="5.336" rx="1" transform="translate(71.537 0)" fill="#4a4a4a"/> 116 - <rect id="Rectangle_50" data-name="Rectangle 50" width="5.336" height="5.336" rx="1" transform="translate(77.86 0)" fill="#4a4a4a"/> 117 - <rect id="Rectangle_51" data-name="Rectangle 51" width="5.336" height="5.336" rx="1" transform="translate(84.183 0)" fill="#4a4a4a"/> 118 - </g> 119 - <g id="Group_6" data-name="Group 6" transform="translate(1.531 16.584)"> 120 - <path id="Path_54" data-name="Path 54" d="M1.093,0h7.3A1.093,1.093,0,0,1,9.485,1.093v3.15A1.093,1.093,0,0,1,8.392,5.336h-7.3A1.093,1.093,0,0,1,0,4.243V1.094A1.093,1.093,0,0,1,1.093,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/> 121 - <g id="Group_5" data-name="Group 5" transform="translate(10.671 0)"> 122 - <rect id="Rectangle_52" data-name="Rectangle 52" width="5.336" height="5.336" rx="1" fill="#4a4a4a"/> 123 - <rect id="Rectangle_53" data-name="Rectangle 53" width="5.336" height="5.336" rx="1" transform="translate(6.324 0)" fill="#4a4a4a"/> 124 - <rect id="Rectangle_54" data-name="Rectangle 54" width="5.336" height="5.336" rx="1" transform="translate(12.647 0)" fill="#4a4a4a"/> 125 - <rect id="Rectangle_55" data-name="Rectangle 55" width="5.336" height="5.336" rx="1" transform="translate(18.971 0)" fill="#4a4a4a"/> 126 - <rect id="Rectangle_56" data-name="Rectangle 56" width="5.336" height="5.336" rx="1" transform="translate(25.295 0)" fill="#4a4a4a"/> 127 - <rect id="Rectangle_57" data-name="Rectangle 57" width="5.336" height="5.336" rx="1" transform="translate(31.619 0)" fill="#4a4a4a"/> 128 - <rect id="Rectangle_58" data-name="Rectangle 58" width="5.336" height="5.336" rx="1" transform="translate(37.942 0)" fill="#4a4a4a"/> 129 - <rect id="Rectangle_59" data-name="Rectangle 59" width="5.336" height="5.336" rx="1" transform="translate(44.265 0)" fill="#4a4a4a"/> 130 - <rect id="Rectangle_60" data-name="Rectangle 60" width="5.336" height="5.336" rx="1" transform="translate(50.589 0)" fill="#4a4a4a"/> 131 - <rect id="Rectangle_61" data-name="Rectangle 61" width="5.336" height="5.336" rx="1" transform="translate(56.912 0)" fill="#4a4a4a"/> 132 - <rect id="Rectangle_62" data-name="Rectangle 62" width="5.336" height="5.336" rx="1" transform="translate(63.236 0)" fill="#4a4a4a"/> 133 - </g> 134 - <path id="Path_55" data-name="Path 55" d="M1.094,0H8A1.093,1.093,0,0,1,9.091,1.093v3.15A1.093,1.093,0,0,1,8,5.336H1.093A1.093,1.093,0,0,1,0,4.243V1.094A1.093,1.093,0,0,1,1.093,0Z" transform="translate(80.428 0)" fill="#4a4a4a" fill-rule="evenodd"/> 135 - </g> 136 - <g id="Group_7" data-name="Group 7" transform="translate(1.531 29.627)"> 137 - <rect id="Rectangle_63" data-name="Rectangle 63" width="5.336" height="5.336" rx="1" transform="translate(0 0)" fill="#4a4a4a"/> 138 - <rect id="Rectangle_64" data-name="Rectangle 64" width="5.336" height="5.336" rx="1" transform="translate(6.324 0)" fill="#4a4a4a"/> 139 - <rect id="Rectangle_65" data-name="Rectangle 65" width="5.336" height="5.336" rx="1" transform="translate(12.647 0)" fill="#4a4a4a"/> 140 - <rect id="Rectangle_66" data-name="Rectangle 66" width="5.336" height="5.336" rx="1" transform="translate(18.971 0)" fill="#4a4a4a"/> 141 - <path id="Path_56" data-name="Path 56" d="M1.093,0H31.515a1.093,1.093,0,0,1,1.093,1.093V4.244a1.093,1.093,0,0,1-1.093,1.093H1.093A1.093,1.093,0,0,1,0,4.244V1.093A1.093,1.093,0,0,1,1.093,0ZM34.687,0h3.942a1.093,1.093,0,0,1,1.093,1.093V4.244a1.093,1.093,0,0,1-1.093,1.093H34.687a1.093,1.093,0,0,1-1.093-1.093V1.093A1.093,1.093,0,0,1,34.687,0Z" transform="translate(25.294 0)" fill="#4a4a4a" fill-rule="evenodd"/> 142 - <rect id="Rectangle_67" data-name="Rectangle 67" width="5.336" height="5.336" rx="1" transform="translate(66.003 0)" fill="#4a4a4a"/> 143 - <rect id="Rectangle_68" data-name="Rectangle 68" width="5.336" height="5.336" rx="1" transform="translate(72.327 0)" fill="#4a4a4a"/> 144 - <rect id="Rectangle_69" data-name="Rectangle 69" width="5.336" height="5.336" rx="1" transform="translate(84.183 0)" fill="#4a4a4a"/> 145 - <path id="Path_57" data-name="Path 57" d="M5.336,0V1.18A1.093,1.093,0,0,1,4.243,2.273H1.093A1.093,1.093,0,0,1,0,1.18V0Z" transform="translate(83.59 2.273) rotate(180)" fill="#4a4a4a"/> 146 - <path id="Path_58" data-name="Path 58" d="M5.336,0V1.18A1.093,1.093,0,0,1,4.243,2.273H1.093A1.093,1.093,0,0,1,0,1.18V0Z" transform="translate(78.255 3.063)" fill="#4a4a4a"/> 147 - </g> 148 - <rect id="Rectangle_70" data-name="Rectangle 70" width="88.927" height="2.371" rx="1.085" transform="translate(1.925 1.17)" fill="#4a4a4a"/> 149 - <rect id="Rectangle_71" data-name="Rectangle 71" width="4.986" height="1.581" rx="0.723" transform="translate(4.1 1.566)" fill="#d8d8d8" opacity="0.136"/> 150 - <rect id="Rectangle_72" data-name="Rectangle 72" width="4.986" height="1.581" rx="0.723" transform="translate(10.923 1.566)" fill="#d8d8d8" opacity="0.136"/> 151 - <rect id="Rectangle_73" data-name="Rectangle 73" width="4.986" height="1.581" rx="0.723" transform="translate(16.173 1.566)" fill="#d8d8d8" opacity="0.136"/> 152 - <rect id="Rectangle_74" data-name="Rectangle 74" width="4.986" height="1.581" rx="0.723" transform="translate(21.421 1.566)" fill="#d8d8d8" opacity="0.136"/> 153 - <rect id="Rectangle_75" data-name="Rectangle 75" width="4.986" height="1.581" rx="0.723" transform="translate(26.671 1.566)" fill="#d8d8d8" opacity="0.136"/> 154 - <rect id="Rectangle_76" data-name="Rectangle 76" width="4.986" height="1.581" rx="0.723" transform="translate(33.232 1.566)" fill="#d8d8d8" opacity="0.136"/> 155 - <rect id="Rectangle_77" data-name="Rectangle 77" width="4.986" height="1.581" rx="0.723" transform="translate(38.48 1.566)" fill="#d8d8d8" opacity="0.136"/> 156 - <rect id="Rectangle_78" data-name="Rectangle 78" width="4.986" height="1.581" rx="0.723" transform="translate(43.73 1.566)" fill="#d8d8d8" opacity="0.136"/> 157 - <rect id="Rectangle_79" data-name="Rectangle 79" width="4.986" height="1.581" rx="0.723" transform="translate(48.978 1.566)" fill="#d8d8d8" opacity="0.136"/> 158 - <rect id="Rectangle_80" data-name="Rectangle 80" width="4.986" height="1.581" rx="0.723" transform="translate(55.54 1.566)" fill="#d8d8d8" opacity="0.136"/> 159 - <rect id="Rectangle_81" data-name="Rectangle 81" width="4.986" height="1.581" rx="0.723" transform="translate(60.788 1.566)" fill="#d8d8d8" opacity="0.136"/> 160 - <rect id="Rectangle_82" data-name="Rectangle 82" width="4.986" height="1.581" rx="0.723" transform="translate(66.038 1.566)" fill="#d8d8d8" opacity="0.136"/> 161 - <rect id="Rectangle_83" data-name="Rectangle 83" width="4.986" height="1.581" rx="0.723" transform="translate(72.599 1.566)" fill="#d8d8d8" opacity="0.136"/> 162 - <rect id="Rectangle_84" data-name="Rectangle 84" width="4.986" height="1.581" rx="0.723" transform="translate(77.847 1.566)" fill="#d8d8d8" opacity="0.136"/> 163 - <rect id="Rectangle_85" data-name="Rectangle 85" width="4.986" height="1.581" rx="0.723" transform="translate(83.097 1.566)" fill="#d8d8d8" opacity="0.136"/> 164 - </g> 165 - <path id="Path_59" data-name="Path 59" d="M146.71,159.855a5.439,5.439,0,0,0-.7.07c-.042-.164-.081-.329-.127-.493a5.457,5.457,0,1,0-5.4-9.372q-.181-.185-.366-.367a5.454,5.454,0,1,0-9.384-5.4c-.162-.046-.325-.084-.486-.126a5.467,5.467,0,1,0-10.788,0c-.162.042-.325.08-.486.126a5.457,5.457,0,1,0-9.384,5.4,21.843,21.843,0,1,0,36.421,21.02,5.452,5.452,0,1,0,.7-10.858" transform="translate(6.275 -6.025)" fill="#44d860" fill-rule="evenodd"/> 166 - <path id="Path_60" data-name="Path 60" d="M83,124.855h43.71V103H83Z" transform="translate(4.42 -9.271)" fill="#3ecc5f" fill-rule="evenodd"/> 167 - <path id="Path_61" data-name="Path 61" d="M134.855,116.765a2.732,2.732,0,1,0,0-5.464,2.811,2.811,0,0,0-.349.035c-.022-.082-.04-.164-.063-.246a2.733,2.733,0,0,0-1.052-5.253,2.7,2.7,0,0,0-1.648.566q-.09-.093-.184-.184a2.7,2.7,0,0,0,.553-1.633,2.732,2.732,0,0,0-5.245-1.07,10.928,10.928,0,1,0,0,21.031,2.732,2.732,0,0,0,5.245-1.07,2.7,2.7,0,0,0-.553-1.633q.093-.09.184-.184a2.7,2.7,0,0,0,1.648.566,2.732,2.732,0,0,0,1.052-5.253c.023-.081.042-.164.063-.246a2.811,2.811,0,0,0,.349.035" transform="translate(7.202 -9.377)" fill="#44d860" fill-rule="evenodd"/> 168 - <path id="Path_62" data-name="Path 62" d="M143.232,42.33a2.967,2.967,0,0,1-.535-.055,2.754,2.754,0,0,1-.514-.153,2.838,2.838,0,0,1-.471-.251,4.139,4.139,0,0,1-.415-.339,3.2,3.2,0,0,1-.338-.415A2.7,2.7,0,0,1,140.5,39.6a2.968,2.968,0,0,1,.055-.535,3.152,3.152,0,0,1,.152-.514,2.874,2.874,0,0,1,.252-.47,2.633,2.633,0,0,1,.753-.754,2.837,2.837,0,0,1,.471-.251,2.753,2.753,0,0,1,.514-.153,2.527,2.527,0,0,1,1.071,0,2.654,2.654,0,0,1,.983.4,4.139,4.139,0,0,1,.415.339,4.019,4.019,0,0,1,.339.415,2.786,2.786,0,0,1,.251.47,2.864,2.864,0,0,1,.208,1.049,2.77,2.77,0,0,1-.8,1.934,4.139,4.139,0,0,1-.415.339,2.722,2.722,0,0,1-1.519.459m21.855-1.366a2.789,2.789,0,0,1-1.935-.8,4.162,4.162,0,0,1-.338-.415,2.7,2.7,0,0,1-.459-1.519,2.789,2.789,0,0,1,.8-1.934,4.139,4.139,0,0,1,.415-.339,2.838,2.838,0,0,1,.471-.251,2.752,2.752,0,0,1,.514-.153,2.527,2.527,0,0,1,1.071,0,2.654,2.654,0,0,1,.983.4,4.139,4.139,0,0,1,.415.339,2.79,2.79,0,0,1,.8,1.934,3.069,3.069,0,0,1-.055.535,2.779,2.779,0,0,1-.153.514,3.885,3.885,0,0,1-.251.47,4.02,4.02,0,0,1-.339.415,4.138,4.138,0,0,1-.415.339,2.722,2.722,0,0,1-1.519.459" transform="translate(9.753 -15.532)" fill-rule="evenodd"/> 169 - </g> 170 - </g> 171 - </svg>
-170
website/static/img/undraw_docusaurus_react.svg
··· 1 - <svg xmlns="http://www.w3.org/2000/svg" width="1041.277" height="554.141" viewBox="0 0 1041.277 554.141"> 2 - <title>Powered by React</title> 3 - <g id="Group_24" data-name="Group 24" transform="translate(-440 -263)"> 4 - <g id="Group_23" data-name="Group 23" transform="translate(439.989 262.965)"> 5 - <path id="Path_299" data-name="Path 299" d="M1040.82,611.12q-1.74,3.75-3.47,7.4-2.7,5.67-5.33,11.12c-.78,1.61-1.56,3.19-2.32,4.77-8.6,17.57-16.63,33.11-23.45,45.89A73.21,73.21,0,0,1,942.44,719l-151.65,1.65h-1.6l-13,.14-11.12.12-34.1.37h-1.38l-17.36.19h-.53l-107,1.16-95.51,1-11.11.12-69,.75H429l-44.75.48h-.48l-141.5,1.53-42.33.46a87.991,87.991,0,0,1-10.79-.54h0c-1.22-.14-2.44-.3-3.65-.49a87.38,87.38,0,0,1-51.29-27.54C116,678.37,102.75,655,93.85,629.64q-1.93-5.49-3.6-11.12C59.44,514.37,97,380,164.6,290.08q4.25-5.64,8.64-11l.07-.08c20.79-25.52,44.1-46.84,68.93-62,44-26.91,92.75-34.49,140.7-11.9,40.57,19.12,78.45,28.11,115.17,30.55,3.71.24,7.42.42,11.11.53,84.23,2.65,163.17-27.7,255.87-47.29,3.69-.78,7.39-1.55,11.12-2.28,66.13-13.16,139.49-20.1,226.73-5.51a189.089,189.089,0,0,1,26.76,6.4q5.77,1.86,11.12,4c41.64,16.94,64.35,48.24,74,87.46q1.37,5.46,2.37,11.11C1134.3,384.41,1084.19,518.23,1040.82,611.12Z" transform="translate(-79.34 -172.91)" fill="#f2f2f2"/> 6 - <path id="Path_300" data-name="Path 300" d="M576.36,618.52a95.21,95.21,0,0,1-1.87,11.12h93.7V618.52Zm-78.25,62.81,11.11-.09V653.77c-3.81-.17-7.52-.34-11.11-.52ZM265.19,618.52v11.12h198.5V618.52ZM1114.87,279h-74V191.51q-5.35-2.17-11.12-4V279H776.21V186.58c-3.73.73-7.43,1.5-11.12,2.28V279H509.22V236.15c-3.69-.11-7.4-.29-11.11-.53V279H242.24V217c-24.83,15.16-48.14,36.48-68.93,62h-.07v.08q-4.4,5.4-8.64,11h8.64V618.52h-83q1.66,5.63,3.6,11.12h79.39v93.62a87,87,0,0,0,12.2,2.79c1.21.19,2.43.35,3.65.49h0a87.991,87.991,0,0,0,10.79.54l42.33-.46v-97H498.11v94.21l11.11-.12V629.64H765.09V721l11.12-.12V629.64H1029.7v4.77c.76-1.58,1.54-3.16,2.32-4.77q2.63-5.45,5.33-11.12,1.73-3.64,3.47-7.4v-321h76.42Q1116.23,284.43,1114.87,279ZM242.24,618.52V290.08H498.11V618.52Zm267,0V290.08H765.09V618.52Zm520.48,0H776.21V290.08H1029.7Z" transform="translate(-79.34 -172.91)" opacity="0.1"/> 7 - <path id="Path_301" data-name="Path 301" d="M863.09,533.65v13l-151.92,1.4-1.62.03-57.74.53-1.38.02-17.55.15h-.52l-106.98.99L349.77,551.4h-.15l-44.65.42-.48.01-198.4,1.82v-15l46.65-28,93.6-.78,2-.01.66-.01,2-.03,44.94-.37,2.01-.01.64-.01,2-.01L315,509.3l.38-.01,35.55-.3h.29l277.4-2.34,6.79-.05h.68l5.18-.05,37.65-.31,2-.03,1.85-.02h.96l11.71-.09,2.32-.03,3.11-.02,9.75-.09,15.47-.13,2-.02,3.48-.02h.65l74.71-.64Z" fill="#65617d"/> 8 - <path id="Path_302" data-name="Path 302" d="M863.09,533.65v13l-151.92,1.4-1.62.03-57.74.53-1.38.02-17.55.15h-.52l-106.98.99L349.77,551.4h-.15l-44.65.42-.48.01-198.4,1.82v-15l46.65-28,93.6-.78,2-.01.66-.01,2-.03,44.94-.37,2.01-.01.64-.01,2-.01L315,509.3l.38-.01,35.55-.3h.29l277.4-2.34,6.79-.05h.68l5.18-.05,37.65-.31,2-.03,1.85-.02h.96l11.71-.09,2.32-.03,3.11-.02,9.75-.09,15.47-.13,2-.02,3.48-.02h.65l74.71-.64Z" opacity="0.2"/> 9 - <path id="Path_303" data-name="Path 303" d="M375.44,656.57v24.49a6.13,6.13,0,0,1-3.5,5.54,6,6,0,0,1-2.5.6l-34.9.74a6,6,0,0,1-2.7-.57,6.12,6.12,0,0,1-3.57-5.57V656.57Z" transform="translate(-79.34 -172.91)" fill="#3f3d56"/> 10 - <path id="Path_304" data-name="Path 304" d="M375.44,656.57v24.49a6.13,6.13,0,0,1-3.5,5.54,6,6,0,0,1-2.5.6l-34.9.74a6,6,0,0,1-2.7-.57,6.12,6.12,0,0,1-3.57-5.57V656.57Z" transform="translate(-79.34 -172.91)" opacity="0.1"/> 11 - <path id="Path_305" data-name="Path 305" d="M377.44,656.57v24.49a6.13,6.13,0,0,1-3.5,5.54,6,6,0,0,1-2.5.6l-34.9.74a6,6,0,0,1-2.7-.57,6.12,6.12,0,0,1-3.57-5.57V656.57Z" transform="translate(-79.34 -172.91)" fill="#3f3d56"/> 12 - <rect id="Rectangle_137" data-name="Rectangle 137" width="47.17" height="31.5" transform="translate(680.92 483.65)" fill="#3f3d56"/> 13 - <rect id="Rectangle_138" data-name="Rectangle 138" width="47.17" height="31.5" transform="translate(680.92 483.65)" opacity="0.1"/> 14 - <rect id="Rectangle_139" data-name="Rectangle 139" width="47.17" height="31.5" transform="translate(678.92 483.65)" fill="#3f3d56"/> 15 - <path id="Path_306" data-name="Path 306" d="M298.09,483.65v4.97l-47.17,1.26v-6.23Z" opacity="0.1"/> 16 - <path id="Path_307" data-name="Path 307" d="M460.69,485.27v168.2a4,4,0,0,1-3.85,3.95l-191.65,5.1h-.05a4,4,0,0,1-3.95-3.95V485.27a4,4,0,0,1,3.95-3.95h191.6a4,4,0,0,1,3.95,3.95Z" transform="translate(-79.34 -172.91)" fill="#65617d"/> 17 - <path id="Path_308" data-name="Path 308" d="M265.19,481.32v181.2h-.05a4,4,0,0,1-3.95-3.95V485.27a4,4,0,0,1,3.95-3.95Z" transform="translate(-79.34 -172.91)" opacity="0.1"/> 18 - <path id="Path_309" data-name="Path 309" d="M194.59,319.15h177.5V467.4l-177.5,4Z" fill="#39374d"/> 19 - <path id="Path_310" data-name="Path 310" d="M726.09,483.65v6.41l-47.17-1.26v-5.15Z" opacity="0.1"/> 20 - <path id="Path_311" data-name="Path 311" d="M867.69,485.27v173.3a4,4,0,0,1-4,3.95h0L672,657.42a4,4,0,0,1-3.85-3.95V485.27a4,4,0,0,1,3.95-3.95H863.7a4,4,0,0,1,3.99,3.95Z" transform="translate(-79.34 -172.91)" fill="#65617d"/> 21 - <path id="Path_312" data-name="Path 312" d="M867.69,485.27v173.3a4,4,0,0,1-4,3.95h0V481.32h0a4,4,0,0,1,4,3.95Z" transform="translate(-79.34 -172.91)" opacity="0.1"/> 22 - <path id="Path_313" data-name="Path 313" d="M775.59,319.15H598.09V467.4l177.5,4Z" fill="#39374d"/> 23 - <path id="Path_314" data-name="Path 314" d="M663.19,485.27v168.2a4,4,0,0,1-3.85,3.95l-191.65,5.1h0a4,4,0,0,1-4-3.95V485.27a4,4,0,0,1,3.95-3.95h191.6A4,4,0,0,1,663.19,485.27Z" transform="translate(-79.34 -172.91)" fill="#65617d"/> 24 - <path id="Path_315" data-name="Path 315" d="M397.09,319.15h177.5V467.4l-177.5,4Z" fill="#4267b2"/> 25 - <path id="Path_316" data-name="Path 316" d="M863.09,533.65v13l-151.92,1.4-1.62.03-57.74.53-1.38.02-17.55.15h-.52l-106.98.99L349.77,551.4h-.15l-44.65.42-.48.01-198.4,1.82v-15l202.51-1.33h.48l40.99-.28h.19l283.08-1.87h.29l.17-.01h.47l4.79-.03h1.46l74.49-.5,4.4-.02.98-.01Z" opacity="0.1"/> 26 - <circle id="Ellipse_111" data-name="Ellipse 111" cx="51.33" cy="51.33" r="51.33" transform="translate(435.93 246.82)" fill="#fbbebe"/> 27 - <path id="Path_317" data-name="Path 317" d="M617.94,550.07s-99.5,12-90,0c3.44-4.34,4.39-17.2,4.2-31.85-.06-4.45-.22-9.06-.45-13.65-1.1-22-3.75-43.5-3.75-43.5s87-41,77-8.5c-4,13.13-2.69,31.57.35,48.88.89,5.05,1.92,10,3,14.7a344.66,344.66,0,0,0,9.65,33.92Z" transform="translate(-79.34 -172.91)" fill="#fbbebe"/> 28 - <path id="Path_318" data-name="Path 318" d="M585.47,546c11.51-2.13,23.7-6,34.53-1.54,2.85,1.17,5.47,2.88,8.39,3.86s6.12,1.22,9.16,1.91c10.68,2.42,19.34,10.55,24.9,20s8.44,20.14,11.26,30.72l6.9,25.83c6,22.45,12,45.09,13.39,68.3a2437.506,2437.506,0,0,1-250.84,1.43c5.44-10.34,11-21.31,10.54-33s-7.19-23.22-4.76-34.74c1.55-7.34,6.57-13.39,9.64-20.22,8.75-19.52,1.94-45.79,17.32-60.65,6.92-6.68,17-9.21,26.63-8.89,12.28.41,24.85,4.24,37,6.11C555.09,547.48,569.79,548.88,585.47,546Z" transform="translate(-79.34 -172.91)" fill="#ff6584"/> 29 - <path id="Path_319" data-name="Path 319" d="M716.37,657.17l-.1,1.43v.1l-.17,2.3-1.33,18.51-1.61,22.3-.46,6.28-1,13.44v.17l-107,1-175.59,1.9v.84h-.14v-1.12l.45-14.36.86-28.06.74-23.79.07-2.37a10.53,10.53,0,0,1,11.42-10.17c4.72.4,10.85.89,18.18,1.41l3,.22c42.33,2.94,120.56,6.74,199.5,2,1.66-.09,3.33-.19,5-.31,12.24-.77,24.47-1.76,36.58-3a10.53,10.53,0,0,1,11.6,11.23Z" transform="translate(-79.34 -172.91)" opacity="0.1"/> 30 - <path id="Path_320" data-name="Path 320" d="M429.08,725.44v-.84l175.62-1.91,107-1h.3v-.17l1-13.44.43-6,1.64-22.61,1.29-17.9v-.44a10.617,10.617,0,0,0-.11-2.47.3.3,0,0,0,0-.1,10.391,10.391,0,0,0-2-4.64,10.54,10.54,0,0,0-9.42-4c-12.11,1.24-24.34,2.23-36.58,3-1.67.12-3.34.22-5,.31-78.94,4.69-157.17.89-199.5-2l-3-.22c-7.33-.52-13.46-1-18.18-1.41a10.54,10.54,0,0,0-11.24,8.53,11,11,0,0,0-.18,1.64l-.68,22.16L429.54,710l-.44,14.36v1.12Z" transform="translate(-79.34 -172.91)" fill="#3f3d56"/> 31 - <path id="Path_321" data-name="Path 321" d="M716.67,664.18l-1.23,15.33-1.83,22.85-.46,5.72-1,12.81-.06.64v.17h0l-.15,1.48.11-1.48h-.29l-107,1-175.65,1.9v-.28l.49-14.36,1-28.06.64-18.65A6.36,6.36,0,0,1,434.3,658a6.25,6.25,0,0,1,3.78-.9c2.1.17,4.68.37,7.69.59,4.89.36,10.92.78,17.94,1.22,13,.82,29.31,1.7,48,2.42,52,2,122.2,2.67,188.88-3.17,3-.26,6.1-.55,9.13-.84a6.26,6.26,0,0,1,3.48.66,5.159,5.159,0,0,1,.86.54,6.14,6.14,0,0,1,2,2.46,3.564,3.564,0,0,1,.25.61A6.279,6.279,0,0,1,716.67,664.18Z" transform="translate(-79.34 -172.91)" opacity="0.1"/> 32 - <path id="Path_322" data-name="Path 322" d="M377.44,677.87v3.19a6.13,6.13,0,0,1-3.5,5.54l-40.1.77a6.12,6.12,0,0,1-3.57-5.57v-3Z" transform="translate(-79.34 -172.91)" opacity="0.1"/> 33 - <path id="Path_323" data-name="Path 323" d="M298.59,515.57l-52.25,1V507.9l52.25-1Z" fill="#3f3d56"/> 34 - <path id="Path_324" data-name="Path 324" d="M298.59,515.57l-52.25,1V507.9l52.25-1Z" opacity="0.1"/> 35 - <path id="Path_325" data-name="Path 325" d="M300.59,515.57l-52.25,1V507.9l52.25-1Z" fill="#3f3d56"/> 36 - <path id="Path_326" data-name="Path 326" d="M758.56,679.87v3.19a6.13,6.13,0,0,0,3.5,5.54l40.1.77a6.12,6.12,0,0,0,3.57-5.57v-3Z" transform="translate(-79.34 -172.91)" opacity="0.1"/> 37 - <path id="Path_327" data-name="Path 327" d="M678.72,517.57l52.25,1V509.9l-52.25-1Z" opacity="0.1"/> 38 - <path id="Path_328" data-name="Path 328" d="M676.72,517.57l52.25,1V509.9l-52.25-1Z" fill="#3f3d56"/> 39 - <path id="Path_329" data-name="Path 329" d="M534.13,486.79c.08,7-3.16,13.6-5.91,20.07a163.491,163.491,0,0,0-12.66,74.71c.73,11,2.58,22,.73,32.9s-8.43,21.77-19,24.9c17.53,10.45,41.26,9.35,57.76-2.66,8.79-6.4,15.34-15.33,21.75-24.11a97.86,97.86,0,0,1-13.31,44.75A103.43,103.43,0,0,0,637,616.53c4.31-5.81,8.06-12.19,9.72-19.23,3.09-13-1.22-26.51-4.51-39.5a266.055,266.055,0,0,1-6.17-33c-.43-3.56-.78-7.22.1-10.7,1-4.07,3.67-7.51,5.64-11.22,5.6-10.54,5.73-23.3,2.86-34.88s-8.49-22.26-14.06-32.81c-4.46-8.46-9.3-17.31-17.46-22.28-5.1-3.1-11-4.39-16.88-5.64l-25.37-5.43c-5.55-1.19-11.26-2.38-16.87-1.51-9.47,1.48-16.14,8.32-22,15.34-4.59,5.46-15.81,15.71-16.6,22.86-.72,6.59,5.1,17.63,6.09,24.58,1.3,9,2.22,6,7.3,11.52C532,478.05,534.07,482,534.13,486.79Z" transform="translate(-79.34 -172.91)" fill="#3f3d56"/> 40 - </g> 41 - <g id="docusaurus_keytar" transform="translate(670.271 615.768)"> 42 - <path id="Path_40" data-name="Path 40" d="M99,52h43.635V69.662H99Z" transform="translate(-49.132 -33.936)" fill="#fff" fill-rule="evenodd"/> 43 - <path id="Path_41" data-name="Path 41" d="M13.389,158.195A10.377,10.377,0,0,1,4.4,153a10.377,10.377,0,0,0,8.988,15.584H23.779V158.195Z" transform="translate(-3 -82.47)" fill="#3ecc5f" fill-rule="evenodd"/> 44 - <path id="Path_42" data-name="Path 42" d="M66.967,38.083l36.373-2.273V30.615A10.389,10.389,0,0,0,92.95,20.226H46.2l-1.3-2.249a1.5,1.5,0,0,0-2.6,0L41,20.226l-1.3-2.249a1.5,1.5,0,0,0-2.6,0l-1.3,2.249-1.3-2.249a1.5,1.5,0,0,0-2.6,0l-1.3,2.249-.034,0-2.152-2.151a1.5,1.5,0,0,0-2.508.672L25.21,21.4l-2.7-.723a1.5,1.5,0,0,0-1.836,1.837l.722,2.7-2.65.71a1.5,1.5,0,0,0-.673,2.509l2.152,2.152c0,.011,0,.022,0,.033l-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6L20.226,41l-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3A10.389,10.389,0,0,0,30.615,103.34H92.95A10.389,10.389,0,0,0,103.34,92.95V51.393L66.967,49.12a5.53,5.53,0,0,1,0-11.038" transform="translate(-9.836 -17.226)" fill="#3ecc5f" fill-rule="evenodd"/> 45 - <path id="Path_43" data-name="Path 43" d="M143,163.779h15.584V143H143Z" transform="translate(-70.275 -77.665)" fill="#3ecc5f" fill-rule="evenodd"/> 46 - <path id="Path_44" data-name="Path 44" d="M173.779,148.389a2.582,2.582,0,0,0-.332.033c-.02-.078-.038-.156-.06-.234a2.594,2.594,0,1,0-2.567-4.455q-.086-.088-.174-.175a2.593,2.593,0,1,0-4.461-2.569c-.077-.022-.154-.04-.231-.06a2.6,2.6,0,1,0-5.128,0c-.077.02-.154.038-.231.06a2.594,2.594,0,1,0-4.461,2.569,10.384,10.384,0,1,0,17.314,9.992,2.592,2.592,0,1,0,.332-5.161" transform="translate(-75.08 -75.262)" fill="#44d860" fill-rule="evenodd"/> 47 - <path id="Path_45" data-name="Path 45" d="M153,113.389h15.584V103H153Z" transform="translate(-75.08 -58.444)" fill="#3ecc5f" fill-rule="evenodd"/> 48 - <path id="Path_46" data-name="Path 46" d="M183.389,108.944a1.3,1.3,0,1,0,0-2.6,1.336,1.336,0,0,0-.166.017c-.01-.039-.019-.078-.03-.117a1.3,1.3,0,0,0-.5-2.5,1.285,1.285,0,0,0-.783.269q-.043-.044-.087-.087a1.285,1.285,0,0,0,.263-.776,1.3,1.3,0,0,0-2.493-.509,5.195,5.195,0,1,0,0,10,1.3,1.3,0,0,0,2.493-.509,1.285,1.285,0,0,0-.263-.776q.044-.043.087-.087a1.285,1.285,0,0,0,.783.269,1.3,1.3,0,0,0,.5-2.5c.011-.038.02-.078.03-.117a1.337,1.337,0,0,0,.166.017" transform="translate(-84.691 -57.894)" fill="#44d860" fill-rule="evenodd"/> 49 - <path id="Path_47" data-name="Path 47" d="M52.188,48.292a1.3,1.3,0,0,1-1.3-1.3,3.9,3.9,0,0,0-7.792,0,1.3,1.3,0,1,1-2.6,0,6.493,6.493,0,0,1,12.987,0,1.3,1.3,0,0,1-1.3,1.3" transform="translate(-21.02 -28.41)" fill-rule="evenodd"/> 50 - <path id="Path_48" data-name="Path 48" d="M103,139.752h31.168a10.389,10.389,0,0,0,10.389-10.389V93H113.389A10.389,10.389,0,0,0,103,103.389Z" transform="translate(-51.054 -53.638)" fill="#ffff50" fill-rule="evenodd"/> 51 - <path id="Path_49" data-name="Path 49" d="M141.1,94.017H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0,10.389H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0,10.389H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0-25.877H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0,10.293H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0,10.389H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m7.782-47.993c-.006,0-.011,0-.018,0-1.605.055-2.365,1.66-3.035,3.077-.7,1.48-1.24,2.443-2.126,2.414-.981-.035-1.542-1.144-2.137-2.317-.683-1.347-1.462-2.876-3.1-2.819-1.582.054-2.344,1.451-3.017,2.684-.715,1.313-1.2,2.112-2.141,2.075-1-.036-1.533-.938-2.149-1.981-.686-1.162-1.479-2.467-3.084-2.423-1.555.053-2.319,1.239-2.994,2.286-.713,1.106-1.213,1.781-2.164,1.741-1.025-.036-1.554-.784-2.167-1.65-.688-.973-1.463-2.074-3.062-2.021a3.815,3.815,0,0,0-2.959,1.879c-.64.812-1.14,1.456-2.2,1.415a.52.52,0,0,0-.037,1.039,3.588,3.588,0,0,0,3.05-1.811c.611-.777,1.139-1.448,2.178-1.483,1-.043,1.47.579,2.179,1.582.674.953,1.438,2.033,2.977,2.089,1.612.054,2.387-1.151,3.074-2.217.614-.953,1.144-1.775,2.156-1.81.931-.035,1.438.7,2.153,1.912.674,1.141,1.437,2.434,3.006,2.491,1.623.056,2.407-1.361,3.09-2.616.592-1.085,1.15-2.109,2.14-2.143.931-.022,1.417.829,2.135,2.249.671,1.326,1.432,2.828,3.026,2.886l.088,0c1.592,0,2.347-1.6,3.015-3.01.592-1.252,1.152-2.431,2.113-2.479Z" transform="translate(-55.378 -38.552)" fill-rule="evenodd"/> 52 - <path id="Path_50" data-name="Path 50" d="M83,163.779h20.779V143H83Z" transform="translate(-41.443 -77.665)" fill="#3ecc5f" fill-rule="evenodd"/> 53 - <g id="Group_8" data-name="Group 8" transform="matrix(0.966, -0.259, 0.259, 0.966, 51.971, 43.3)"> 54 - <rect id="Rectangle_3" data-name="Rectangle 3" width="43.906" height="17.333" rx="2" transform="translate(0 0)" fill="#d8d8d8"/> 55 - <g id="Group_2" data-name="Group 2" transform="translate(0.728 10.948)"> 56 - <rect id="Rectangle_4" data-name="Rectangle 4" width="2.537" height="2.537" rx="1" transform="translate(7.985 0)" fill="#4a4a4a"/> 57 - <rect id="Rectangle_5" data-name="Rectangle 5" width="2.537" height="2.537" rx="1" transform="translate(10.991 0)" fill="#4a4a4a"/> 58 - <rect id="Rectangle_6" data-name="Rectangle 6" width="2.537" height="2.537" rx="1" transform="translate(13.997 0)" fill="#4a4a4a"/> 59 - <rect id="Rectangle_7" data-name="Rectangle 7" width="2.537" height="2.537" rx="1" transform="translate(17.003 0)" fill="#4a4a4a"/> 60 - <rect id="Rectangle_8" data-name="Rectangle 8" width="2.537" height="2.537" rx="1" transform="translate(20.009 0)" fill="#4a4a4a"/> 61 - <rect id="Rectangle_9" data-name="Rectangle 9" width="2.537" height="2.537" rx="1" transform="translate(23.015 0)" fill="#4a4a4a"/> 62 - <rect id="Rectangle_10" data-name="Rectangle 10" width="2.537" height="2.537" rx="1" transform="translate(26.021 0)" fill="#4a4a4a"/> 63 - <rect id="Rectangle_11" data-name="Rectangle 11" width="2.537" height="2.537" rx="1" transform="translate(29.028 0)" fill="#4a4a4a"/> 64 - <rect id="Rectangle_12" data-name="Rectangle 12" width="2.537" height="2.537" rx="1" transform="translate(32.034 0)" fill="#4a4a4a"/> 65 - <path id="Path_51" data-name="Path 51" d="M.519,0H6.9A.519.519,0,0,1,7.421.52v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.519A.519.519,0,0,1,.519,0ZM35.653,0h6.383a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H35.652a.519.519,0,0,1-.519-.519V.519A.519.519,0,0,1,35.652,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/> 66 - </g> 67 - <g id="Group_3" data-name="Group 3" transform="translate(0.728 4.878)"> 68 - <path id="Path_52" data-name="Path 52" d="M.519,0H2.956a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.519A.519.519,0,0,1,.519,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/> 69 - <rect id="Rectangle_13" data-name="Rectangle 13" width="2.537" height="2.537" rx="1" transform="translate(3.945 0)" fill="#4a4a4a"/> 70 - <rect id="Rectangle_14" data-name="Rectangle 14" width="2.537" height="2.537" rx="1" transform="translate(6.951 0)" fill="#4a4a4a"/> 71 - <rect id="Rectangle_15" data-name="Rectangle 15" width="2.537" height="2.537" rx="1" transform="translate(9.958 0)" fill="#4a4a4a"/> 72 - <rect id="Rectangle_16" data-name="Rectangle 16" width="2.537" height="2.537" rx="1" transform="translate(12.964 0)" fill="#4a4a4a"/> 73 - <rect id="Rectangle_17" data-name="Rectangle 17" width="2.537" height="2.537" rx="1" transform="translate(15.97 0)" fill="#4a4a4a"/> 74 - <rect id="Rectangle_18" data-name="Rectangle 18" width="2.537" height="2.537" rx="1" transform="translate(18.976 0)" fill="#4a4a4a"/> 75 - <rect id="Rectangle_19" data-name="Rectangle 19" width="2.537" height="2.537" rx="1" transform="translate(21.982 0)" fill="#4a4a4a"/> 76 - <rect id="Rectangle_20" data-name="Rectangle 20" width="2.537" height="2.537" rx="1" transform="translate(24.988 0)" fill="#4a4a4a"/> 77 - <rect id="Rectangle_21" data-name="Rectangle 21" width="2.537" height="2.537" rx="1" transform="translate(27.994 0)" fill="#4a4a4a"/> 78 - <rect id="Rectangle_22" data-name="Rectangle 22" width="2.537" height="2.537" rx="1" transform="translate(31 0)" fill="#4a4a4a"/> 79 - <rect id="Rectangle_23" data-name="Rectangle 23" width="2.537" height="2.537" rx="1" transform="translate(34.006 0)" fill="#4a4a4a"/> 80 - <rect id="Rectangle_24" data-name="Rectangle 24" width="2.537" height="2.537" rx="1" transform="translate(37.012 0)" fill="#4a4a4a"/> 81 - <rect id="Rectangle_25" data-name="Rectangle 25" width="2.537" height="2.537" rx="1" transform="translate(40.018 0)" fill="#4a4a4a"/> 82 - </g> 83 - <g id="Group_4" data-name="Group 4" transform="translate(43.283 4.538) rotate(180)"> 84 - <path id="Path_53" data-name="Path 53" d="M.519,0H2.956a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.519A.519.519,0,0,1,.519,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/> 85 - <rect id="Rectangle_26" data-name="Rectangle 26" width="2.537" height="2.537" rx="1" transform="translate(3.945 0)" fill="#4a4a4a"/> 86 - <rect id="Rectangle_27" data-name="Rectangle 27" width="2.537" height="2.537" rx="1" transform="translate(6.951 0)" fill="#4a4a4a"/> 87 - <rect id="Rectangle_28" data-name="Rectangle 28" width="2.537" height="2.537" rx="1" transform="translate(9.958 0)" fill="#4a4a4a"/> 88 - <rect id="Rectangle_29" data-name="Rectangle 29" width="2.537" height="2.537" rx="1" transform="translate(12.964 0)" fill="#4a4a4a"/> 89 - <rect id="Rectangle_30" data-name="Rectangle 30" width="2.537" height="2.537" rx="1" transform="translate(15.97 0)" fill="#4a4a4a"/> 90 - <rect id="Rectangle_31" data-name="Rectangle 31" width="2.537" height="2.537" rx="1" transform="translate(18.976 0)" fill="#4a4a4a"/> 91 - <rect id="Rectangle_32" data-name="Rectangle 32" width="2.537" height="2.537" rx="1" transform="translate(21.982 0)" fill="#4a4a4a"/> 92 - <rect id="Rectangle_33" data-name="Rectangle 33" width="2.537" height="2.537" rx="1" transform="translate(24.988 0)" fill="#4a4a4a"/> 93 - <rect id="Rectangle_34" data-name="Rectangle 34" width="2.537" height="2.537" rx="1" transform="translate(27.994 0)" fill="#4a4a4a"/> 94 - <rect id="Rectangle_35" data-name="Rectangle 35" width="2.537" height="2.537" rx="1" transform="translate(31.001 0)" fill="#4a4a4a"/> 95 - <rect id="Rectangle_36" data-name="Rectangle 36" width="2.537" height="2.537" rx="1" transform="translate(34.007 0)" fill="#4a4a4a"/> 96 - <rect id="Rectangle_37" data-name="Rectangle 37" width="2.537" height="2.537" rx="1" transform="translate(37.013 0)" fill="#4a4a4a"/> 97 - <rect id="Rectangle_38" data-name="Rectangle 38" width="2.537" height="2.537" rx="1" transform="translate(40.018 0)" fill="#4a4a4a"/> 98 - <rect id="Rectangle_39" data-name="Rectangle 39" width="2.537" height="2.537" rx="1" transform="translate(3.945 0)" fill="#4a4a4a"/> 99 - <rect id="Rectangle_40" data-name="Rectangle 40" width="2.537" height="2.537" rx="1" transform="translate(6.951 0)" fill="#4a4a4a"/> 100 - <rect id="Rectangle_41" data-name="Rectangle 41" width="2.537" height="2.537" rx="1" transform="translate(9.958 0)" fill="#4a4a4a"/> 101 - <rect id="Rectangle_42" data-name="Rectangle 42" width="2.537" height="2.537" rx="1" transform="translate(12.964 0)" fill="#4a4a4a"/> 102 - <rect id="Rectangle_43" data-name="Rectangle 43" width="2.537" height="2.537" rx="1" transform="translate(15.97 0)" fill="#4a4a4a"/> 103 - <rect id="Rectangle_44" data-name="Rectangle 44" width="2.537" height="2.537" rx="1" transform="translate(18.976 0)" fill="#4a4a4a"/> 104 - <rect id="Rectangle_45" data-name="Rectangle 45" width="2.537" height="2.537" rx="1" transform="translate(21.982 0)" fill="#4a4a4a"/> 105 - <rect id="Rectangle_46" data-name="Rectangle 46" width="2.537" height="2.537" rx="1" transform="translate(24.988 0)" fill="#4a4a4a"/> 106 - <rect id="Rectangle_47" data-name="Rectangle 47" width="2.537" height="2.537" rx="1" transform="translate(27.994 0)" fill="#4a4a4a"/> 107 - <rect id="Rectangle_48" data-name="Rectangle 48" width="2.537" height="2.537" rx="1" transform="translate(31.001 0)" fill="#4a4a4a"/> 108 - <rect id="Rectangle_49" data-name="Rectangle 49" width="2.537" height="2.537" rx="1" transform="translate(34.007 0)" fill="#4a4a4a"/> 109 - <rect id="Rectangle_50" data-name="Rectangle 50" width="2.537" height="2.537" rx="1" transform="translate(37.013 0)" fill="#4a4a4a"/> 110 - <rect id="Rectangle_51" data-name="Rectangle 51" width="2.537" height="2.537" rx="1" transform="translate(40.018 0)" fill="#4a4a4a"/> 111 - </g> 112 - <g id="Group_6" data-name="Group 6" transform="translate(0.728 7.883)"> 113 - <path id="Path_54" data-name="Path 54" d="M.519,0h3.47a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.52A.519.519,0,0,1,.519,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/> 114 - <g id="Group_5" data-name="Group 5" transform="translate(5.073 0)"> 115 - <rect id="Rectangle_52" data-name="Rectangle 52" width="2.537" height="2.537" rx="1" transform="translate(0 0)" fill="#4a4a4a"/> 116 - <rect id="Rectangle_53" data-name="Rectangle 53" width="2.537" height="2.537" rx="1" transform="translate(3.006 0)" fill="#4a4a4a"/> 117 - <rect id="Rectangle_54" data-name="Rectangle 54" width="2.537" height="2.537" rx="1" transform="translate(6.012 0)" fill="#4a4a4a"/> 118 - <rect id="Rectangle_55" data-name="Rectangle 55" width="2.537" height="2.537" rx="1" transform="translate(9.018 0)" fill="#4a4a4a"/> 119 - <rect id="Rectangle_56" data-name="Rectangle 56" width="2.537" height="2.537" rx="1" transform="translate(12.025 0)" fill="#4a4a4a"/> 120 - <rect id="Rectangle_57" data-name="Rectangle 57" width="2.537" height="2.537" rx="1" transform="translate(15.031 0)" fill="#4a4a4a"/> 121 - <rect id="Rectangle_58" data-name="Rectangle 58" width="2.537" height="2.537" rx="1" transform="translate(18.037 0)" fill="#4a4a4a"/> 122 - <rect id="Rectangle_59" data-name="Rectangle 59" width="2.537" height="2.537" rx="1" transform="translate(21.042 0)" fill="#4a4a4a"/> 123 - <rect id="Rectangle_60" data-name="Rectangle 60" width="2.537" height="2.537" rx="1" transform="translate(24.049 0)" fill="#4a4a4a"/> 124 - <rect id="Rectangle_61" data-name="Rectangle 61" width="2.537" height="2.537" rx="1" transform="translate(27.055 0)" fill="#4a4a4a"/> 125 - <rect id="Rectangle_62" data-name="Rectangle 62" width="2.537" height="2.537" rx="1" transform="translate(30.061 0)" fill="#4a4a4a"/> 126 - </g> 127 - <path id="Path_55" data-name="Path 55" d="M.52,0H3.8a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.52A.519.519,0,0,1,.519,0Z" transform="translate(38.234 0)" fill="#4a4a4a" fill-rule="evenodd"/> 128 - </g> 129 - <g id="Group_7" data-name="Group 7" transform="translate(0.728 14.084)"> 130 - <rect id="Rectangle_63" data-name="Rectangle 63" width="2.537" height="2.537" rx="1" transform="translate(0 0)" fill="#4a4a4a"/> 131 - <rect id="Rectangle_64" data-name="Rectangle 64" width="2.537" height="2.537" rx="1" transform="translate(3.006 0)" fill="#4a4a4a"/> 132 - <rect id="Rectangle_65" data-name="Rectangle 65" width="2.537" height="2.537" rx="1" transform="translate(6.012 0)" fill="#4a4a4a"/> 133 - <rect id="Rectangle_66" data-name="Rectangle 66" width="2.537" height="2.537" rx="1" transform="translate(9.018 0)" fill="#4a4a4a"/> 134 - <path id="Path_56" data-name="Path 56" d="M.519,0H14.981A.519.519,0,0,1,15.5.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.018V.519A.519.519,0,0,1,.519,0Zm15.97,0h1.874a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H16.489a.519.519,0,0,1-.519-.519V.519A.519.519,0,0,1,16.489,0Z" transform="translate(12.024 0)" fill="#4a4a4a" fill-rule="evenodd"/> 135 - <rect id="Rectangle_67" data-name="Rectangle 67" width="2.537" height="2.537" rx="1" transform="translate(31.376 0)" fill="#4a4a4a"/> 136 - <rect id="Rectangle_68" data-name="Rectangle 68" width="2.537" height="2.537" rx="1" transform="translate(34.382 0)" fill="#4a4a4a"/> 137 - <rect id="Rectangle_69" data-name="Rectangle 69" width="2.537" height="2.537" rx="1" transform="translate(40.018 0)" fill="#4a4a4a"/> 138 - <path id="Path_57" data-name="Path 57" d="M2.537,0V.561a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,.561V0Z" transform="translate(39.736 1.08) rotate(180)" fill="#4a4a4a"/> 139 - <path id="Path_58" data-name="Path 58" d="M2.537,0V.561a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,.561V0Z" transform="translate(37.2 1.456)" fill="#4a4a4a"/> 140 - </g> 141 - <rect id="Rectangle_70" data-name="Rectangle 70" width="42.273" height="1.127" rx="0.564" transform="translate(0.915 0.556)" fill="#4a4a4a"/> 142 - <rect id="Rectangle_71" data-name="Rectangle 71" width="2.37" height="0.752" rx="0.376" transform="translate(1.949 0.744)" fill="#d8d8d8" opacity="0.136"/> 143 - <rect id="Rectangle_72" data-name="Rectangle 72" width="2.37" height="0.752" rx="0.376" transform="translate(5.193 0.744)" fill="#d8d8d8" opacity="0.136"/> 144 - <rect id="Rectangle_73" data-name="Rectangle 73" width="2.37" height="0.752" rx="0.376" transform="translate(7.688 0.744)" fill="#d8d8d8" opacity="0.136"/> 145 - <rect id="Rectangle_74" data-name="Rectangle 74" width="2.37" height="0.752" rx="0.376" transform="translate(10.183 0.744)" fill="#d8d8d8" opacity="0.136"/> 146 - <rect id="Rectangle_75" data-name="Rectangle 75" width="2.37" height="0.752" rx="0.376" transform="translate(12.679 0.744)" fill="#d8d8d8" opacity="0.136"/> 147 - <rect id="Rectangle_76" data-name="Rectangle 76" width="2.37" height="0.752" rx="0.376" transform="translate(15.797 0.744)" fill="#d8d8d8" opacity="0.136"/> 148 - <rect id="Rectangle_77" data-name="Rectangle 77" width="2.37" height="0.752" rx="0.376" transform="translate(18.292 0.744)" fill="#d8d8d8" opacity="0.136"/> 149 - <rect id="Rectangle_78" data-name="Rectangle 78" width="2.37" height="0.752" rx="0.376" transform="translate(20.788 0.744)" fill="#d8d8d8" opacity="0.136"/> 150 - <rect id="Rectangle_79" data-name="Rectangle 79" width="2.37" height="0.752" rx="0.376" transform="translate(23.283 0.744)" fill="#d8d8d8" opacity="0.136"/> 151 - <rect id="Rectangle_80" data-name="Rectangle 80" width="2.37" height="0.752" rx="0.376" transform="translate(26.402 0.744)" fill="#d8d8d8" opacity="0.136"/> 152 - <rect id="Rectangle_81" data-name="Rectangle 81" width="2.37" height="0.752" rx="0.376" transform="translate(28.897 0.744)" fill="#d8d8d8" opacity="0.136"/> 153 - <rect id="Rectangle_82" data-name="Rectangle 82" width="2.37" height="0.752" rx="0.376" transform="translate(31.393 0.744)" fill="#d8d8d8" opacity="0.136"/> 154 - <rect id="Rectangle_83" data-name="Rectangle 83" width="2.37" height="0.752" rx="0.376" transform="translate(34.512 0.744)" fill="#d8d8d8" opacity="0.136"/> 155 - <rect id="Rectangle_84" data-name="Rectangle 84" width="2.37" height="0.752" rx="0.376" transform="translate(37.007 0.744)" fill="#d8d8d8" opacity="0.136"/> 156 - <rect id="Rectangle_85" data-name="Rectangle 85" width="2.37" height="0.752" rx="0.376" transform="translate(39.502 0.744)" fill="#d8d8d8" opacity="0.136"/> 157 - </g> 158 - <path id="Path_59" data-name="Path 59" d="M123.779,148.389a2.583,2.583,0,0,0-.332.033c-.02-.078-.038-.156-.06-.234a2.594,2.594,0,1,0-2.567-4.455q-.086-.088-.174-.175a2.593,2.593,0,1,0-4.461-2.569c-.077-.022-.154-.04-.231-.06a2.6,2.6,0,1,0-5.128,0c-.077.02-.154.038-.231.06a2.594,2.594,0,1,0-4.461,2.569,10.384,10.384,0,1,0,17.314,9.992,2.592,2.592,0,1,0,.332-5.161" transform="translate(-51.054 -75.262)" fill="#44d860" fill-rule="evenodd"/> 159 - <path id="Path_60" data-name="Path 60" d="M83,113.389h20.779V103H83Z" transform="translate(-41.443 -58.444)" fill="#3ecc5f" fill-rule="evenodd"/> 160 - <path id="Path_61" data-name="Path 61" d="M123.389,108.944a1.3,1.3,0,1,0,0-2.6,1.338,1.338,0,0,0-.166.017c-.01-.039-.019-.078-.03-.117a1.3,1.3,0,0,0-.5-2.5,1.285,1.285,0,0,0-.783.269q-.043-.044-.087-.087a1.285,1.285,0,0,0,.263-.776,1.3,1.3,0,0,0-2.493-.509,5.195,5.195,0,1,0,0,10,1.3,1.3,0,0,0,2.493-.509,1.285,1.285,0,0,0-.263-.776q.044-.043.087-.087a1.285,1.285,0,0,0,.783.269,1.3,1.3,0,0,0,.5-2.5c.011-.038.02-.078.03-.117a1.335,1.335,0,0,0,.166.017" transform="translate(-55.859 -57.894)" fill="#44d860" fill-rule="evenodd"/> 161 - <path id="Path_62" data-name="Path 62" d="M141.8,38.745a1.41,1.41,0,0,1-.255-.026,1.309,1.309,0,0,1-.244-.073,1.349,1.349,0,0,1-.224-.119,1.967,1.967,0,0,1-.2-.161,1.52,1.52,0,0,1-.161-.2,1.282,1.282,0,0,1-.218-.722,1.41,1.41,0,0,1,.026-.255,1.5,1.5,0,0,1,.072-.244,1.364,1.364,0,0,1,.12-.223,1.252,1.252,0,0,1,.358-.358,1.349,1.349,0,0,1,.224-.119,1.309,1.309,0,0,1,.244-.073,1.2,1.2,0,0,1,.509,0,1.262,1.262,0,0,1,.468.192,1.968,1.968,0,0,1,.2.161,1.908,1.908,0,0,1,.161.2,1.322,1.322,0,0,1,.12.223,1.361,1.361,0,0,1,.1.5,1.317,1.317,0,0,1-.379.919,1.968,1.968,0,0,1-.2.161,1.346,1.346,0,0,1-.223.119,1.332,1.332,0,0,1-.5.1m10.389-.649a1.326,1.326,0,0,1-.92-.379,1.979,1.979,0,0,1-.161-.2,1.282,1.282,0,0,1-.218-.722,1.326,1.326,0,0,1,.379-.919,1.967,1.967,0,0,1,.2-.161,1.351,1.351,0,0,1,.224-.119,1.308,1.308,0,0,1,.244-.073,1.2,1.2,0,0,1,.509,0,1.262,1.262,0,0,1,.468.192,1.967,1.967,0,0,1,.2.161,1.326,1.326,0,0,1,.379.919,1.461,1.461,0,0,1-.026.255,1.323,1.323,0,0,1-.073.244,1.847,1.847,0,0,1-.119.223,1.911,1.911,0,0,1-.161.2,1.967,1.967,0,0,1-.2.161,1.294,1.294,0,0,1-.722.218" transform="translate(-69.074 -26.006)" fill-rule="evenodd"/> 162 - </g> 163 - <g id="React-icon" transform="translate(906.3 541.56)"> 164 - <path id="Path_330" data-name="Path 330" d="M263.668,117.179c0-5.827-7.3-11.35-18.487-14.775,2.582-11.4,1.434-20.477-3.622-23.382a7.861,7.861,0,0,0-4.016-1v4a4.152,4.152,0,0,1,2.044.466c2.439,1.4,3.5,6.724,2.672,13.574-.2,1.685-.52,3.461-.914,5.272a86.9,86.9,0,0,0-11.386-1.954,87.469,87.469,0,0,0-7.459-8.965c5.845-5.433,11.332-8.41,15.062-8.41V78h0c-4.931,0-11.386,3.514-17.913,9.611-6.527-6.061-12.982-9.539-17.913-9.539v4c3.712,0,9.216,2.959,15.062,8.356a84.687,84.687,0,0,0-7.405,8.947,83.732,83.732,0,0,0-11.4,1.972c-.412-1.793-.717-3.532-.932-5.2-.843-6.85.2-12.175,2.618-13.592a3.991,3.991,0,0,1,2.062-.466v-4h0a8,8,0,0,0-4.052,1c-5.039,2.9-6.168,11.96-3.568,23.328-11.153,3.443-18.415,8.947-18.415,14.757,0,5.828,7.3,11.35,18.487,14.775-2.582,11.4-1.434,20.477,3.622,23.382a7.882,7.882,0,0,0,4.034,1c4.931,0,11.386-3.514,17.913-9.611,6.527,6.061,12.982,9.539,17.913,9.539a8,8,0,0,0,4.052-1c5.039-2.9,6.168-11.96,3.568-23.328C256.406,128.511,263.668,122.988,263.668,117.179Zm-23.346-11.96c-.663,2.313-1.488,4.7-2.421,7.083-.735-1.434-1.506-2.869-2.349-4.3-.825-1.434-1.7-2.833-2.582-4.2C235.517,104.179,237.974,104.645,240.323,105.219Zm-8.212,19.1c-1.4,2.421-2.833,4.716-4.321,6.85-2.672.233-5.379.359-8.1.359-2.708,0-5.415-.126-8.069-.341q-2.232-3.2-4.339-6.814-2.044-3.523-3.73-7.136c1.112-2.4,2.367-4.805,3.712-7.154,1.4-2.421,2.833-4.716,4.321-6.85,2.672-.233,5.379-.359,8.1-.359,2.708,0,5.415.126,8.069.341q2.232,3.2,4.339,6.814,2.044,3.523,3.73,7.136C234.692,119.564,233.455,121.966,232.11,124.315Zm5.792-2.331c.968,2.4,1.793,4.805,2.474,7.136-2.349.574-4.823,1.058-7.387,1.434.879-1.381,1.757-2.8,2.582-4.25C236.4,124.871,237.167,123.419,237.9,121.984ZM219.72,141.116a73.921,73.921,0,0,1-4.985-5.738c1.614.072,3.263.126,4.931.126,1.685,0,3.353-.036,4.985-.126A69.993,69.993,0,0,1,219.72,141.116ZM206.38,130.555c-2.546-.377-5-.843-7.352-1.417.663-2.313,1.488-4.7,2.421-7.083.735,1.434,1.506,2.869,2.349,4.3S205.5,129.192,206.38,130.555ZM219.63,93.241a73.924,73.924,0,0,1,4.985,5.738c-1.614-.072-3.263-.126-4.931-.126-1.686,0-3.353.036-4.985.126A69.993,69.993,0,0,1,219.63,93.241ZM206.362,103.8c-.879,1.381-1.757,2.8-2.582,4.25-.825,1.434-1.6,2.869-2.331,4.3-.968-2.4-1.793-4.805-2.474-7.136C201.323,104.663,203.8,104.179,206.362,103.8Zm-16.227,22.449c-6.348-2.708-10.454-6.258-10.454-9.073s4.106-6.383,10.454-9.073c1.542-.663,3.228-1.255,4.967-1.811a86.122,86.122,0,0,0,4.034,10.92,84.9,84.9,0,0,0-3.981,10.866C193.38,127.525,191.694,126.915,190.134,126.252Zm9.647,25.623c-2.439-1.4-3.5-6.724-2.672-13.574.2-1.686.52-3.461.914-5.272a86.9,86.9,0,0,0,11.386,1.954,87.465,87.465,0,0,0,7.459,8.965c-5.845,5.433-11.332,8.41-15.062,8.41A4.279,4.279,0,0,1,199.781,151.875Zm42.532-13.663c.843,6.85-.2,12.175-2.618,13.592a3.99,3.99,0,0,1-2.062.466c-3.712,0-9.216-2.959-15.062-8.356a84.689,84.689,0,0,0,7.405-8.947,83.731,83.731,0,0,0,11.4-1.972A50.194,50.194,0,0,1,242.313,138.212Zm6.9-11.96c-1.542.663-3.228,1.255-4.967,1.811a86.12,86.12,0,0,0-4.034-10.92,84.9,84.9,0,0,0,3.981-10.866c1.775.556,3.461,1.165,5.039,1.829,6.348,2.708,10.454,6.258,10.454,9.073C259.67,119.994,255.564,123.562,249.216,126.252Z" fill="#61dafb"/> 165 - <path id="Path_331" data-name="Path 331" d="M320.8,78.4Z" transform="translate(-119.082 -0.328)" fill="#61dafb"/> 166 - <circle id="Ellipse_112" data-name="Ellipse 112" cx="8.194" cy="8.194" r="8.194" transform="translate(211.472 108.984)" fill="#61dafb"/> 167 - <path id="Path_332" data-name="Path 332" d="M520.5,78.1Z" transform="translate(-282.975 -0.082)" fill="#61dafb"/> 168 - </g> 169 - </g> 170 - </svg>
-40
website/static/img/undraw_docusaurus_tree.svg
··· 1 - <svg xmlns="http://www.w3.org/2000/svg" width="1129" height="663" viewBox="0 0 1129 663"> 2 - <title>Focus on What Matters</title> 3 - <circle cx="321" cy="321" r="321" fill="#f2f2f2" /> 4 - <ellipse cx="559" cy="635.49998" rx="514" ry="27.50002" fill="#3f3d56" /> 5 - <ellipse cx="558" cy="627" rx="460" ry="22" opacity="0.2" /> 6 - <rect x="131" y="152.5" width="840" height="50" fill="#3f3d56" /> 7 - <path d="M166.5,727.3299A21.67009,21.67009,0,0,0,188.1701,749H984.8299A21.67009,21.67009,0,0,0,1006.5,727.3299V296h-840Z" transform="translate(-35.5 -118.5)" fill="#3f3d56" /> 8 - <path d="M984.8299,236H188.1701A21.67009,21.67009,0,0,0,166.5,257.6701V296h840V257.6701A21.67009,21.67009,0,0,0,984.8299,236Z" transform="translate(-35.5 -118.5)" fill="#3f3d56" /> 9 - <path d="M984.8299,236H188.1701A21.67009,21.67009,0,0,0,166.5,257.6701V296h840V257.6701A21.67009,21.67009,0,0,0,984.8299,236Z" transform="translate(-35.5 -118.5)" opacity="0.2" /> 10 - <circle cx="181" cy="147.5" r="13" fill="#3f3d56" /> 11 - <circle cx="217" cy="147.5" r="13" fill="#3f3d56" /> 12 - <circle cx="253" cy="147.5" r="13" fill="#3f3d56" /> 13 - <rect x="168" y="213.5" width="337" height="386" rx="5.33505" fill="#606060" /> 14 - <rect x="603" y="272.5" width="284" height="22" rx="5.47638" fill="#2e8555" /> 15 - <rect x="537" y="352.5" width="416" height="15" rx="5.47638" fill="#2e8555" /> 16 - <rect x="537" y="396.5" width="416" height="15" rx="5.47638" fill="#2e8555" /> 17 - <rect x="537" y="440.5" width="416" height="15" rx="5.47638" fill="#2e8555" /> 18 - <rect x="537" y="484.5" width="416" height="15" rx="5.47638" fill="#2e8555" /> 19 - <rect x="865" y="552.5" width="88" height="26" rx="7.02756" fill="#3ecc5f" /> 20 - <path d="M1088.60287,624.61594a30.11371,30.11371,0,0,0,3.98291-15.266c0-13.79652-8.54358-24.98081-19.08256-24.98081s-19.08256,11.18429-19.08256,24.98081a30.11411,30.11411,0,0,0,3.98291,15.266,31.248,31.248,0,0,0,0,30.53213,31.248,31.248,0,0,0,0,30.53208,31.248,31.248,0,0,0,0,30.53208,30.11408,30.11408,0,0,0-3.98291,15.266c0,13.79652,8.54353,24.98081,19.08256,24.98081s19.08256-11.18429,19.08256-24.98081a30.11368,30.11368,0,0,0-3.98291-15.266,31.248,31.248,0,0,0,0-30.53208,31.248,31.248,0,0,0,0-30.53208,31.248,31.248,0,0,0,0-30.53213Z" transform="translate(-35.5 -118.5)" fill="#3f3d56" /> 21 - <ellipse cx="1038.00321" cy="460.31783" rx="19.08256" ry="24.9808" fill="#3f3d56" /> 22 - <ellipse cx="1038.00321" cy="429.78574" rx="19.08256" ry="24.9808" fill="#3f3d56" /> 23 - <path d="M1144.93871,339.34489a91.61081,91.61081,0,0,0,7.10658-10.46092l-50.141-8.23491,54.22885.4033a91.566,91.566,0,0,0,1.74556-72.42605l-72.75449,37.74139,67.09658-49.32086a91.41255,91.41255,0,1,0-150.971,102.29805,91.45842,91.45842,0,0,0-10.42451,16.66946l65.0866,33.81447-69.40046-23.292a91.46011,91.46011,0,0,0,14.73837,85.83669,91.40575,91.40575,0,1,0,143.68892,0,91.41808,91.41808,0,0,0,0-113.02862Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" /> 24 - <path d="M981.6885,395.8592a91.01343,91.01343,0,0,0,19.56129,56.51431,91.40575,91.40575,0,1,0,143.68892,0C1157.18982,436.82067,981.6885,385.60008,981.6885,395.8592Z" transform="translate(-35.5 -118.5)" opacity="0.1" /> 25 - <path d="M365.62,461.43628H477.094v45.12043H365.62Z" transform="translate(-35.5 -118.5)" fill="#fff" fill-rule="evenodd" /> 26 - <path d="M264.76252,608.74122a26.50931,26.50931,0,0,1-22.96231-13.27072,26.50976,26.50976,0,0,0,22.96231,39.81215H291.304V608.74122Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" /> 27 - <path d="M384.17242,468.57061l92.92155-5.80726V449.49263a26.54091,26.54091,0,0,0-26.54143-26.54143H331.1161l-3.31768-5.74622a3.83043,3.83043,0,0,0-6.63536,0l-3.31768,5.74622-3.31767-5.74622a3.83043,3.83043,0,0,0-6.63536,0l-3.31768,5.74622L301.257,417.205a3.83043,3.83043,0,0,0-6.63536,0L291.304,422.9512c-.02919,0-.05573.004-.08625.004l-5.49674-5.49541a3.8293,3.8293,0,0,0-6.4071,1.71723l-1.81676,6.77338L270.607,424.1031a3.82993,3.82993,0,0,0-4.6912,4.69253l1.84463,6.89148-6.77072,1.81411a3.8315,3.8315,0,0,0-1.71988,6.40975l5.49673,5.49673c0,.02787-.004.05574-.004.08493l-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74621,3.31768L259.0163,466.081a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31767a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31767a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83042,3.83042,0,0,0,0,6.63535l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768L259.0163,558.976a3.83042,3.83042,0,0,0,0,6.63535l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83042,3.83042,0,0,0,0,6.63535l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768A26.54091,26.54091,0,0,0,291.304,635.28265H450.55254A26.5409,26.5409,0,0,0,477.094,608.74122V502.5755l-92.92155-5.80727a14.12639,14.12639,0,0,1,0-28.19762" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" /> 28 - <path d="M424.01111,635.28265h39.81214V582.19979H424.01111Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" /> 29 - <path d="M490.36468,602.10586a6.60242,6.60242,0,0,0-.848.08493c-.05042-.19906-.09821-.39945-.15393-.59852A6.62668,6.62668,0,1,0,482.80568,590.21q-.2203-.22491-.44457-.44589a6.62391,6.62391,0,1,0-11.39689-6.56369c-.1964-.05575-.39414-.10218-.59056-.15262a6.63957,6.63957,0,1,0-13.10086,0c-.1964.05042-.39414.09687-.59056.15262a6.62767,6.62767,0,1,0-11.39688,6.56369,26.52754,26.52754,0,1,0,44.23127,25.52756,6.6211,6.6211,0,1,0,.848-13.18579" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" /> 30 - <path d="M437.28182,555.65836H477.094V529.11693H437.28182Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" /> 31 - <path d="M490.36468,545.70532a3.31768,3.31768,0,0,0,0-6.63536,3.41133,3.41133,0,0,0-.42333.04247c-.02655-.09953-.04911-.19907-.077-.29859a3.319,3.319,0,0,0-1.278-6.37923,3.28174,3.28174,0,0,0-2.00122.68742q-.10947-.11346-.22294-.22295a3.282,3.282,0,0,0,.67149-1.98265,3.31768,3.31768,0,0,0-6.37-1.2992,13.27078,13.27078,0,1,0,0,25.54082,3.31768,3.31768,0,0,0,6.37-1.2992,3.282,3.282,0,0,0-.67149-1.98265q.11347-.10947.22294-.22294a3.28174,3.28174,0,0,0,2.00122.68742,3.31768,3.31768,0,0,0,1.278-6.37923c.02786-.0982.05042-.19907.077-.29859a3.41325,3.41325,0,0,0,.42333.04246" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" /> 32 - <path d="M317.84538,466.081a3.31768,3.31768,0,0,1-3.31767-3.31768,9.953,9.953,0,1,0-19.90608,0,3.31768,3.31768,0,1,1-6.63535,0,16.58839,16.58839,0,1,1,33.17678,0,3.31768,3.31768,0,0,1-3.31768,3.31768" transform="translate(-35.5 -118.5)" fill-rule="evenodd" /> 33 - <path d="M370.92825,635.28265h79.62429A26.5409,26.5409,0,0,0,477.094,608.74122v-92.895H397.46968a26.54091,26.54091,0,0,0-26.54143,26.54143Z" transform="translate(-35.5 -118.5)" fill="#ffff50" fill-rule="evenodd" /> 34 - <path d="M457.21444,556.98543H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.54143H390.80778a1.32707,1.32707,0,1,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.54143H390.80778a1.32707,1.32707,0,1,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0-66.10674H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.29459H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.54143H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414M477.094,474.19076c-.01592,0-.0292-.008-.04512-.00663-4.10064.13934-6.04083,4.24132-7.75274,7.86024-1.78623,3.78215-3.16771,6.24122-5.43171,6.16691-2.50685-.09024-3.94007-2.92222-5.45825-5.91874-1.74377-3.44243-3.73438-7.34667-7.91333-7.20069-4.04227.138-5.98907,3.70784-7.70631,6.857-1.82738,3.35484-3.07084,5.39455-5.46887,5.30033-2.55727-.09289-3.91619-2.39536-5.48877-5.06013-1.75306-2.96733-3.77951-6.30359-7.8775-6.18946-3.97326.13669-5.92537,3.16507-7.64791,5.83912-1.82207,2.82666-3.09872,4.5492-5.52725,4.447-2.61832-.09289-3.9706-2.00388-5.53522-4.21611-1.757-2.4856-3.737-5.299-7.82308-5.16231-3.88567.13271-5.83779,2.61434-7.559,4.80135-1.635,2.07555-2.9116,3.71846-5.61218,3.615a1.32793,1.32793,0,1,0-.09555,2.65414c4.00377.134,6.03154-2.38873,7.79257-4.6275,1.562-1.9853,2.91027-3.69855,5.56441-3.78879,2.55594-.10882,3.75429,1.47968,5.56707,4.04093,1.7212,2.43385,3.67465,5.19416,7.60545,5.33616,4.11789.138,6.09921-2.93946,7.8536-5.66261,1.56861-2.43385,2.92221-4.53461,5.50734-4.62352,2.37944-.08892,3.67466,1.79154,5.50072,4.885,1.72121,2.91557,3.67069,6.21865,7.67977,6.36463,4.14709.14332,6.14965-3.47693,7.89475-6.68181,1.51155-2.77092,2.93814-5.38791,5.46621-5.4755,2.37944-.05573,3.62025,2.11668,5.45558,5.74622,1.71459,3.388,3.65875,7.22591,7.73019,7.37321l.22429.004c4.06614,0,5.99571-4.08074,7.70364-7.68905,1.51154-3.19825,2.94211-6.21069,5.3972-6.33411Z" transform="translate(-35.5 -118.5)" fill-rule="evenodd" /> 35 - <path d="M344.38682,635.28265h53.08286V582.19979H344.38682Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" /> 36 - <path d="M424.01111,602.10586a6.60242,6.60242,0,0,0-.848.08493c-.05042-.19906-.09821-.39945-.15394-.59852A6.62667,6.62667,0,1,0,416.45211,590.21q-.2203-.22491-.44458-.44589a6.62391,6.62391,0,1,0-11.39689-6.56369c-.1964-.05575-.39413-.10218-.59054-.15262a6.63957,6.63957,0,1,0-13.10084,0c-.19641.05042-.39414.09687-.59055.15262a6.62767,6.62767,0,1,0-11.39689,6.56369,26.52755,26.52755,0,1,0,44.2313,25.52756,6.6211,6.6211,0,1,0,.848-13.18579" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" /> 37 - <path d="M344.38682,555.65836h53.08286V529.11693H344.38682Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" /> 38 - <path d="M410.74039,545.70532a3.31768,3.31768,0,1,0,0-6.63536,3.41133,3.41133,0,0,0-.42333.04247c-.02655-.09953-.04911-.19907-.077-.29859a3.319,3.319,0,0,0-1.278-6.37923,3.28174,3.28174,0,0,0-2.00122.68742q-.10947-.11346-.22294-.22295a3.282,3.282,0,0,0,.67149-1.98265,3.31768,3.31768,0,0,0-6.37-1.2992,13.27078,13.27078,0,1,0,0,25.54082,3.31768,3.31768,0,0,0,6.37-1.2992,3.282,3.282,0,0,0-.67149-1.98265q.11347-.10947.22294-.22294a3.28174,3.28174,0,0,0,2.00122.68742,3.31768,3.31768,0,0,0,1.278-6.37923c.02786-.0982.05042-.19907.077-.29859a3.41325,3.41325,0,0,0,.42333.04246" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" /> 39 - <path d="M424.01111,447.8338a3.60349,3.60349,0,0,1-.65028-.06636,3.34415,3.34415,0,0,1-.62372-.18579,3.44679,3.44679,0,0,1-.572-.30522,5.02708,5.02708,0,0,1-.50429-.4114,3.88726,3.88726,0,0,1-.41007-.50428,3.27532,3.27532,0,0,1-.55737-1.84463,3.60248,3.60248,0,0,1,.06636-.65027,3.82638,3.82638,0,0,1,.18447-.62373,3.48858,3.48858,0,0,1,.30656-.57064,3.197,3.197,0,0,1,.91436-.91568,3.44685,3.44685,0,0,1,.572-.30523,3.344,3.344,0,0,1,.62372-.18578,3.06907,3.06907,0,0,1,1.30053,0,3.22332,3.22332,0,0,1,1.19436.491,5.02835,5.02835,0,0,1,.50429.41139,4.8801,4.8801,0,0,1,.41139.50429,3.38246,3.38246,0,0,1,.30522.57064,3.47806,3.47806,0,0,1,.25215,1.274A3.36394,3.36394,0,0,1,426.36,446.865a5.02708,5.02708,0,0,1-.50429.4114,3.3057,3.3057,0,0,1-1.84463.55737m26.54143-1.65884a3.38754,3.38754,0,0,1-2.35024-.96877,5.04185,5.04185,0,0,1-.41007-.50428,3.27532,3.27532,0,0,1-.55737-1.84463,3.38659,3.38659,0,0,1,.96744-2.34892,5.02559,5.02559,0,0,1,.50429-.41139,3.44685,3.44685,0,0,1,.572-.30523,3.3432,3.3432,0,0,1,.62373-.18579,3.06952,3.06952,0,0,1,1.30052,0,3.22356,3.22356,0,0,1,1.19436.491,5.02559,5.02559,0,0,1,.50429.41139,3.38792,3.38792,0,0,1,.96876,2.34892,3.72635,3.72635,0,0,1-.06636.65026,3.37387,3.37387,0,0,1-.18579.62373,4.71469,4.71469,0,0,1-.30522.57064,4.8801,4.8801,0,0,1-.41139.50429,5.02559,5.02559,0,0,1-.50429.41139,3.30547,3.30547,0,0,1-1.84463.55737" transform="translate(-35.5 -118.5)" fill-rule="evenodd" /> 40 - </svg>