Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ eligible interactive commands check for a new stable release at most once a day
| `search [keywords] [--filter k=v]...` | search around your location; `search filters` lists valid keys |
| `category list` | category ids |
| `alert list` | your saved searches on wallapop; `watch add search --from-alert ID --name NAME` turns one into a watch |
| `item show / open / favorite / unfavorite` | inspect a listing by hash or url |
| `item show / images / open / favorite / unfavorite` | inspect a listing by hash or url |
| `item reserve / sold / delete` | act on your own listings (`sold`, `delete` ask or need `--yes`) |
| `user show / items / reviews` | look at a seller by hash, url or slug |
| `me show / items / favorites` | your account |
Expand Down Expand Up @@ -207,6 +207,7 @@ wallapop completion powershell >> $PROFILE
wallapop search "thinkpad x1" --max-price 400 --sort newest --format pretty
wallapop search --category 100 --filter brand=Toyota --filter max_km=120000 --limit 20
wallapop item show https://es.wallapop.com/item/thinkpad-x1-carbon-1092837465
wallapop item images https://es.wallapop.com/item/thinkpad-x1-carbon-1092837465 --format pretty

wallapop watch add search "thinkpad x1" --max-price 400 --name x1 --notify phone
wallapop watch check --all --format jsonl | jq -r 'select(.type=="item.price_changed") | .item.url'
Expand Down
18 changes: 18 additions & 0 deletions docs-site/content/docs/reference/item.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,24 @@ Add the listing to your favorites
wallapop item favorite ITEM
```

## `wallapop item images`

Direct image links in gallery order

```bash
wallapop item images ITEM
```

Return direct image URLs for an item hash or full item URL.
JSON returns an array of URL strings; JSONL returns one JSON string per line.
Use --format pretty for one plain URL per line. This reads item metadata,
without downloading the images.

```bash
wallapop item images k2j3h4g5f6d7 --format pretty
wallapop item show k2j3h4g5f6d7 | jq -r '.images[]'
```

## `wallapop item open`

Open the listing in the browser
Expand Down
5 changes: 5 additions & 0 deletions docs/cli-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ detail with the page flags.
- The item detail endpoint is the API of record: title, description, condition and price come from it, empty and zero included, and the rendered page contributes only what it alone knows (flags, counters, slug and the full taxonomy path). The page is a cached snapshot and lags writes. Both reference forms take the same merge, so a hash and its URL always report the same listing.
- `item show ITEM` merges `GET /api/v3/items/{hash}` with the page flags (reserved, sold,
expired, modified date, views, favorites).
- `item images ITEM` returns the direct image URLs in gallery order, using the same
item read as `item show`. JSON is an array of URL strings; JSONL emits one string
per line; pretty emits one unadorned URL per line. No images returns an empty
array (or no lines in JSONL/pretty). `item show --format pretty` also lists each
image URL without truncation. These commands read metadata, not image bytes.
- `item open ITEM` opens the web page in the browser.
- `item favorite ITEM` / `item unfavorite ITEM` via `PUT /api/v3/items/{hash}/favorite`.
- `item reserve ITEM [--off]`, `item sold ITEM [--yes]`, `item delete ITEM [--yes]` act on the
Expand Down
65 changes: 65 additions & 0 deletions internal/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,71 @@ func TestItemShowPrefersDetailOverAStalePage(t *testing.T) {
}
}

// Image extraction must keep the detail gallery order, even when the page
// has an older gallery, and plain output must contain only complete links.
func TestItemImages(t *testing.T) {
h := newHarness(t)
it := h.fake.AddItem(fakewallapop.Item{
Hash: "hashsssstale", Title: "Fresh", Images: 2, PageImages: 5,
})
want := []string{"https://cdn/hashsssstale-1.jpg", "https://cdn/hashsssstale-2.jpg"}
for _, ref := range []string{it.Hash, "https://es.wallapop.com/item/" + it.Slug} {
for _, format := range []string{"json", "jsonl", "pretty", "toon"} {
r := h.must("", "item", "images", ref, "--format", format)
switch format {
case "json":
var urls []string
decode(t, r.stdout, &urls)
if len(urls) != len(want) || urls[0] != want[0] || urls[1] != want[1] {
t.Fatalf("image URLs = %v, want %v", urls, want)
}
case "jsonl":
if r.stdout != fmt.Sprintf("%q\n%q\n", want[0], want[1]) {
t.Fatalf("JSONL = %q", r.stdout)
}
case "pretty":
if r.stdout != strings.Join(want, "\n")+"\n" {
t.Fatalf("plain links = %q", r.stdout)
}
case "toon":
if !strings.Contains(r.stdout, want[0]) || !strings.Contains(r.stdout, want[1]) || strings.Contains(r.stdout, "-3.jpg") {
t.Fatalf("TOON = %q", r.stdout)
}
}
}
shown := h.must("", "item", "show", ref, "--format", "pretty")
for _, url := range want {
if !strings.Contains(shown.stdout, url) {
t.Fatalf("pretty item omitted %s: %s", url, shown.stdout)
}
}
}
for _, tc := range []struct {
ref string
code int
}{
{"1301645280", 2}, {"zzzzzzzzzzzz", 4},
} {
r := h.run("", "item", "images", tc.ref)
if r.code != tc.code || r.stdout != "" {
t.Fatalf("images %s: exit %d, stdout %q, stderr %s", tc.ref, r.code, r.stdout, r.stderr)
}
}
}

func TestItemImagesEmptyGallery(t *testing.T) {
h := newHarness(t)
it := h.fake.AddItem(fakewallapop.Item{Hash: "hashnoimages", Images: -1})
for _, tc := range []struct{ format, want string }{
{"json", "[]\n"}, {"jsonl", ""}, {"pretty", ""},
} {
r := h.must("", "item", "images", it.Hash, "--format", tc.format)
if r.stdout != tc.want {
t.Fatalf("empty gallery in %s: got %q, want %q", tc.format, r.stdout, tc.want)
}
}
}

func TestItemShowByHashAndByURLAgree(t *testing.T) {
h := newHarness(t)
it := h.fake.AddItem(fakewallapop.Item{Hash: "hashbbbbbbbb", Title: "Trek Marlin", Price: 300, Reserved: true})
Expand Down
33 changes: 33 additions & 0 deletions internal/cli/item.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ import (
"github.com/Microck/wallapop-cli/internal/wallapop"
)

// imageURLs keeps the machine formats as a simple array and pretty output
// suitable for copying or piping, without labels or shortened links.
type imageURLs []string

func (urls imageURLs) Pretty(w io.Writer, color bool) {
for _, url := range urls {
fmt.Fprintln(w, url)
}
}

type itemView wallapop.Item

func (v itemView) Pretty(w io.Writer, color bool) {
Expand Down Expand Up @@ -58,6 +68,9 @@ func (v itemView) Pretty(w io.Writer, color bool) {
rows = append(rows, []string{"modified", it.ModifiedAt.Local().Format(time.RFC3339)})
}
rows = append(rows, []string{"url", it.URL})
for i, url := range it.Images {
rows = append(rows, []string{fmt.Sprintf("image %d", i+1), url})
}
output.Table(w, color, nil, rows)
if it.Description != "" {
fmt.Fprintln(w)
Expand All @@ -83,6 +96,26 @@ es.wallapop.com/item/... URL.`,
return a.Printer.Print(itemView(it))
},
},
&cobra.Command{
Use: "images ITEM", Short: "Direct image links in gallery order", Args: cobra.ExactArgs(1),
Long: `Return direct image URLs for an item hash or full item URL.
JSON returns an array of URL strings; JSONL returns one JSON string per line.
Use --format pretty for one plain URL per line. This reads item metadata,
without downloading the images.

Examples:
wallapop item images k2j3h4g5f6d7 --format pretty
wallapop item show k2j3h4g5f6d7 | jq -r '.images[]'`,
RunE: func(cmd *cobra.Command, args []string) error {
it, err := a.Client.Item(cmd.Context(), args[0])
if err != nil {
return err
}
// A successful item with no images is [], never JSON null.
urls := append(imageURLs{}, it.Images...)
return a.Printer.Print(urls)
},
},
&cobra.Command{
Use: "open ITEM", Short: "Open the listing in the browser", Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
Expand Down
7 changes: 5 additions & 2 deletions internal/fakewallapop/fake.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ type Item struct {
Description string
Price float64
Condition string
Images int
Images int // zero defaults to one image; negative means an empty gallery
// Attrs are the listing's category attributes beyond the common four,
// served under type_attributes and rewritten by edits.
Attrs map[string]string
Expand Down Expand Up @@ -393,7 +393,10 @@ func (s *Server) savedSearches(w http.ResponseWriter, r *http.Request) {
}

func fakeImages(hash string, n int) []map[string]any {
if n <= 0 {
if n < 0 {
return []map[string]any{}
}
if n == 0 {
n = 1
}
imgs := make([]map[string]any, 0, n)
Expand Down
Loading