Duell (Duell Bike Center)
Finský dodavatel moto/ATV/sněžných/lodních dílů a doplňků. Na rozdíl od všech
ostatních dodavatelů v tomto projektu negeneruje feed přes URL — data se
stahují přes FTP jako sada ;-oddělených CSV souborů.
Přístup
| Parametr | Hodnota |
|---|---|
| Host | updateftp.duell.fi |
| Uživatel | duellus |
| Heslo | WebUpdate! |
Zdroj: email "Feedy Duell" (17.2.2026, přeposlaný od info@ctyrkolky-vrchlabi.cz),
příloha Product information to customers.pdf. Na FTP navíc leží novější a
přesnější Duell_FTP_fileshare_technical_description_v1.0.pdf (v.1.0, 11.2.2026,
"First set of product information from updated eCom system") — implementace
vychází z něj, ne ze starší zákaznické PDF (ta popisuje starší strukturu
/ProductInformation/{Duell,SDBC}/..., která na serveru sice pořád existuje,
ale nová /Data/ struktura ji nahrazuje).
Rozsah — celý katalog
Import zahrnuje celý katalog Duellu, napříč všemi 8 hlavními
kategoriemi (potomci kořenové kategorie ecom25 v categories.csv):
motorcycle, snowmobile, moped_scooter, atv_utv, marine, bicycle,
garden_forest, campaigns. Produkt zařazený do víc kategorií (a takových je
hodně) dostane breadcrumby pro všechny z nich, ne jen pro jednu.
Původně byl import omezen jen na atv_utv (~16 tis. produktů) jako
konzervativní start — viz commit historie. Rozšířeno na celý katalog na
žádost; k tomu bylo potřeba přejít z DOMDocument na XMLWriter (viz níže),
protože jinak by strom DOM objektů pro ~125 tis. položek byl srovnatelně
paměťově náročný jako u Motopointu (docs/feeds/motopoint.md), kde to na
produkčním serveru (jen ~1,9 GB RAM) opakovaně spadlo na nedostatek paměti.
Živě ověřeno (reálná FTP data, php -d memory_limit=512M):
| Feed | Počet položek | Doba | Špička paměti |
|---|---|---|---|
products |
125 454 | ~146 s | 367,8 MB |
availability |
118 815 | ~13 s | 212,2 MB |
Obě čísla se pohodlně vejdou do produkčního výchozího memory_limit = 512M
(docker/php.ini) — na rozdíl od Motopointu tu nebylo potřeba přesouvat
úložiště produktů do Postgresu ani zvyšovat limit paměti. Hlavní rozdíl:
Duell už předtím zpracovával CSV řádek po řádku přes fgetcsv() (nikdy
nedržel celý soubor v paměti najednou) a lookupy pro sklad/ceny/obrázky jsou
úzké záznamy (pár krátkých polí), ne bohatá data s HTML popisy jako u
Motopointu — jediný skutečný viník byl DOMDocument strom, který XMLWriter
řeší.
Riziko souběhu s Motopointem: oba dodavatelé mají
products: 12 hoursa žádný globální (napříč dodavateli) zámek na "jen jeden náročný refresh najednou" neexistuje — jen per-supplier zámek zFeedRefresher. Pokud by se Motopointův refresh (až ~768 MB) a Duellův (~370 MB) sešly na stejném cronovém tiku, dohromady by na 1,9GB hostu mohly tlačit na hranu i se zbytkem kontejnerů (Postgres, nginx, ...). Zatím nepozorováno jako reálný problém, ale stojí za sledování.
Zdrojové soubory (aktuální /Data/ struktura)
| Soubor | Cesta na FTP | Účel | Update interval | Velikost (celý katalog) |
|---|---|---|---|---|
| Produkty | /Data/Products/products.csv |
Plný katalog, ~190 sloupců | 1× denně | ~190–200 MB / ~125 tis. řádků (mění se, katalog žije) |
| Kategorie | /Data/categories.csv |
code;label-*;parent;updated — plochý seznam s přímým ukazatelem na rodiče |
1× týdně | malý (1 424 řádků) |
| Obrázky | /Data/product_images.csv |
PRODUCT_CODE;IMAGE_URL;IMAGE_NAME — víc řádků na produkt |
1× denně | ~15 MB / 313 787 řádků |
| Sklad | /ic_CSV.csv |
PRODUCT_CODE;VAASA;SWEDEN;TOTAL |
30 min | ~3.7 MB / 148 997 řádků |
| Ceny | /Retail_pricelist.csv |
PRODUCT_ID;PRODUCT_CODE;EUR_EXCL_VAT;EUR_INCL_VAT;EUR_EXCL_VAT_EU;EUR_INCL_VAT_EU;SEK_*;NOK_*;GBP_NET;DKK_* |
2 hodiny | ~17 MB / 143 591 řádků |
Existuje i /ic_ean_CSV.csv (stejné sloupce + EAN) a starší
/ProductInformation/{Duell,SDBC}/... strom — ani jeden se nepoužívá, protože
ic_CSV.csv má klíč PRODUCT_CODE shodný se vším ostatním a nová /Data/
struktura je jednodušší a aktuálnější.
Data podporují stránkování/timestamp inkrementů (products_updates_*.csv,
denní delty za posledních 7 dní) — tento import je zatím nevyužívá, každý
refresh stahuje celý products.csv znovu (stejně jako u všech ostatních
dodavatelů v tomto projektu).
Dvouprůchodový import + streamovaný výstup
fetchProducts() prochází products.csv dvakrát, ale stahuje ho jen
jednou:
- 1. průchod (
collectProductCategories()) — pro každýproduct_codezjistí, jaké kategorie má přiřazené (žádné filtrování — jde jen o to znát množinu reálných kódů a jejich kategorie). - Podle té množiny se stahují a filtrují
ic_CSV.csv,Retail_pricelist.csvaproduct_images.csv— vyřadí se jen záznamy pro kódy, které vproducts.csvvůbec neexistují. - 2. průchod — znovu se čte stejný už stažený dočasný soubor a staví se
výstupní XML pro každý zachycený
product_code.
Výstupní XML se staví přes XMLWriter (streamuje do plochého bufferu),
ne DOMDocument — pro ~125 tis. položek by strom DOMElement/DOMText
objektů byl zbytečná paměťová režie navíc k datům, která už jednou prošla
přes fgetcsv(). fetchAvailability() používá stejný přístup, i když je
per-položku o dost lehčí (jen 5 plochých polí, žádné vnořené PHOTOS/PARAMETERS).
Mapování polí (products.csv → products.xml)
| Výstupní pole | Zdroj |
|---|---|
CODE / PRODUCT_CODE |
product_code — sdílený klíč napříč všemi soubory |
EAN |
barcode |
PRODUCT |
product_name-en, fallback short_description-en |
DESCRIPTION |
long_description-en, fallback short_description-en |
SHORT_DESCRIPTION |
short_description-en |
PRICE |
Retail_pricelist.csv → EUR_EXCL_VAT_EU (viz níže, proč ne EUR_EXCL_VAT) |
VAT |
natvrdo 21 (česká sazba) — Duell neposílá sazbu DPH na produkt, jen předpočítané ceny s/bez DPH pro různé měny/trhy |
HIDDEN |
vždy Y |
CATEGORY |
viz níže |
IN_STORE |
ic_CSV.csv → TOTAL, cross-referenced podle product_code (products.csv sklad neobsahuje) |
PRODUCER |
brand (pozor: hodnoty jsou lowercase kódy, ne hezky formátované názvy — např. metzeler, sram) |
PHOTOS/PHOTO |
product_images.csv → IMAGE_URL, může být víc obrázků na produkt |
PARAMETERS/PARAMETER |
generický průchod přes zbylé sloupce — viz níže |
Proč EUR_EXCL_VAT_EU, ne EUR_EXCL_VAT
Retail_pricelist.csv má dvě sady EUR cen: EUR_EXCL_VAT/EUR_INCL_VAT jsou
pro Nordics/Baltics zákazníky, EUR_EXCL_VAT_EU/EUR_INCL_VAT_EU pro
"outside Nordics/Baltics" — tedy i ČR. Použita je proto ta druhá dvojice
(bez DPH varianta).
Kategorie
categories.csv je plochý seznam s přímým parent odkazem (ne strom jako u
Motopointu). Cesta se staví procházením parent řetězu nahoru od každé
kategorie produktu, dokud se nenarazí na syntetický kořen ecom25 (ten se do
cesty nezahrnuje). Kořen výstupní cesty je vždy Duell.
Pole categories v products.csv je čárkami oddělený seznam kódů kategorií
(produkt může patřit do víc kategorií najednou, včetně různých hlavních
větví zároveň — např. atv_utv i motorcycle). Do breadcrumbs se zahrnou
všechny.
Generické PARAMETERS
products.csv má ~150 sloupců specifikací (rozměry pneumatik, přileb,
baterií, atd. — viz Duell_FTP_fileshare_technical_description_v1.0.pdf pro
plný seznam). Ruční mapování všech by bylo neúměrné, takže se použije obecné
pravidlo:
- Sloupce mapované na pojmenovaná pole výše (
product_code,categories,barcode,brand, popisy, jména,vendor_name, interní/ERP pole jakocreated/updated/parent/vak_codeapod.) se do PARAMETERS nedávají. - Zbytek se stává PARAMETER s
name= syrový název sloupce (např.tyre_width,handlebar_clamp_diameter) — bez hezkých českých názvů. - Páry
X+X-unit(např.product_weight+product_weight-unit) se spojí do jedné hodnoty:"5.4000 KILOGRAM". - Varianty
-fi/-sv(finština/švédština) se zahazují ve prospěch-en(čeština k dispozici není, angličtina je nejbližší použitelná náhrada); přípona-ense z názvu parametru odstraní. - Prázdné hodnoty se přeskakují.
Neřešeno: attribute_options.csv na FTP mapuje kódované hodnoty atributů
(např. bag_type=tank_bag) na hezké popisky ("Tank Bag" / "Tankkilaukku" /
"Tankväska") — momentálně se nepoužívá, takže některé PARAMETER hodnoty budou
syrové kódy místo čitelného textu. Možné budoucí vylepšení.
Ošetření vstupních dat
- Desetinná čárka:
PRICEa skladové sloupce používají evropský formát s čárkou (23,8645) — převádí se na tečku. - Neplatné XML znaky: zdrojová CSV data občas obsahují řídicí znaky mimo
rozsah povolený XML 1.0 (ověřeno živě — narazilo na to
long_description-enu jednoho produktu).DOMDocument::saveXML()je bez varování zapíše, ale zpětné čtení přesXMLReader(používá hofetchAvailability()přesbuildProductLookup()) na nich spadne s parser errorem. Všechen text z CSV se proto před vložením do XML sanitizuje (sanitizeXmlText()).
Zdroj dat pro dostupnost
Živě se stahuje ic_CSV.csv (sklad) a Retail_pricelist.csv (cena); EAN se
bere z mezipaměti posledního produktového XML. Emitují se jen položky, které
existují v cachi products.xml (stejné pravidlo jako u ostatních dodavatelů).
Harmonogram
| Feed | Interval | Poznámka |
|---|---|---|
| Produkty | 12 hodin | Duell aktualizuje products.csv jen 1× denně, ale 12h drží konzistenci s ostatními dodavateli v projektu |
| Dostupnost | 30 minut | Odpovídá skutečnému update intervalu ic_CSV.csv/Retail_pricelist.csv u Duellu (ostatní dodavatelé mají 15 min, ale u nich se skladem aktualizuje častěji) |
Závislosti
Vyžaduje PHP rozšíření ext-ftp (docker-php-ext-install ftp — v Dockerfile
tohoto projektu už je nainstalované, přidáno i do composer.json).