Skip to content
Closed
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
4 changes: 2 additions & 2 deletions backend/services/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ async def set_imagelist_cache(gid: int, data: dict) -> None:

async def get_preview_cache(gid: int) -> dict | None:
"""Returns {str(page_num): preview_url_or_sprite_info} or None."""
return await get_json(f"eh:previews:{gid}")
return await get_json(f"eh:previews:v2:{gid}")


async def set_preview_cache(gid: int, data: dict) -> None:
await set_json(f"eh:previews:{gid}", data, _TTL_IMAGELIST)
await set_json(f"eh:previews:v2:{gid}", data, _TTL_IMAGELIST)


async def get_proxied_image(gid: int, page: int) -> bytes | None:
Expand Down
42 changes: 22 additions & 20 deletions backend/services/eh_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,22 +51,22 @@
r'<img[^>]*alt="(\d+)"[^>]*src="([^"]+)"',
re.DOTALL,
)
# Normal preview: <div class="gdtm" style="...background:...url(SPRITE) -Xpx...;width:Wpx;height:Hpx">
# We extract the sprite URL, offset, width, height AND the page number from the /s/ link inside
# Normal preview: <div class="gdtm" style="...background:...url(SPRITE) -Xpx -Ypx...;width:Wpx;height:Hpx">
# We extract the sprite URL, X/Y offsets, width, height AND the page number from the /s/ link inside
_NORMAL_PREVIEW_RE = re.compile(
r'<div[^>]*class="gdtm"[^>]*style="[^"]*'
r'url\(([^)]+)\)\s*(-?\d+)px[^"]*'
r'url\(([^)]+)\)\s*(-?\d+)px\s+(-?\d+)px[^"]*'
r'width:\s*(\d+)px;\s*height:\s*(\d+)px[^"]*"[^>]*>'
r".*?/s/[0-9a-f]+/\d+-(\d+)",
re.DOTALL,
)
# New format (2024+): <a href="/s/PTOKEN/GID-PAGE"><div style="width:Wpx;height:Hpx;background:transparent url(THUMB) OFFSETpx 0 no-repeat"></div></a>
# Order in style: width → height → background (with url + offset)
# New format (2024+): <a href="/s/PTOKEN/GID-PAGE"><div style="width:Wpx;height:Hpx;background:transparent url(THUMB) Xpx Ypx no-repeat"></div></a>
# Order in style: width → height → background (with url + X/Y offsets)
_NEW_PREVIEW_RE = re.compile(
r'<a[^>]+href="[^"]+/s/[0-9a-f]{10}/\d+-(\d+)"[^>]*>'
r'<div[^>]+style="[^"]*'
r'width:\s*(\d+)px[^"]*height:\s*(\d+)px[^"]*'
r'url\(([^)]+)\)\s*(-?\d+)px',
r'url\(([^)]+)\)\s*(-?\d+)px\s+(-?\d+)px',
re.DOTALL,
)

Expand Down Expand Up @@ -296,35 +296,36 @@ def _parse_detail_html(self, html: str) -> tuple[dict[int, str], dict[int, str]]
# Extract preview thumbnails — try new format first (2024+), then legacy formats
new_matches = list(_NEW_PREVIEW_RE.finditer(html))
if new_matches:
# New format: background url() with optional sprite offset
# Groups: (page_num, width, height, thumb_url, offset_x)
# New format: background url() with sprite offsets
# Groups: (page_num, width, height, thumb_url, offset_x, offset_y)
for match in new_matches:
page_num = int(match.group(1))
width = int(match.group(2))
height = int(match.group(3))
thumb_url = match.group(4)
offset_x = int(match.group(5))
# Always store as sprite format — even offset 0 is part of the sprite sheet
preview_map[page_num] = f"{thumb_url}|{offset_x}|{width}|{height}"
offset_y = int(match.group(6))
# Always store as sprite format — even zero offsets are part of the sprite sheet
preview_map[page_num] = f"{thumb_url}|{offset_x}|{offset_y}|{width}|{height}"

# Normalize cell heights per sprite URL — the sprite image has a single
# height, but CSS may declare different heights for individual cells
# (e.g., cover page 150px vs normal 278px). Use max height per sprite.
sprite_heights: dict[str, int] = defaultdict(int)
for page_num, val in preview_map.items():
parts = val.split('|')
if len(parts) == 4:
if len(parts) == 5:
sprite_url = parts[0]
h = int(parts[3])
h = int(parts[4])
if h > sprite_heights[sprite_url]:
sprite_heights[sprite_url] = h
for page_num in list(preview_map.keys()):
parts = preview_map[page_num].split('|')
if len(parts) == 4:
if len(parts) == 5:
sprite_url = parts[0]
max_h = sprite_heights[sprite_url]
if int(parts[3]) != max_h:
preview_map[page_num] = f"{parts[0]}|{parts[1]}|{parts[2]}|{max_h}"
if int(parts[4]) != max_h:
preview_map[page_num] = f"{parts[0]}|{parts[1]}|{parts[2]}|{parts[3]}|{max_h}"
else:
# Legacy large previews: <div class="gdtl"><img alt="N" src="URL">
large_matches = list(_LARGE_PREVIEW_RE.finditer(html))
Expand All @@ -335,14 +336,15 @@ def _parse_detail_html(self, html: str) -> tuple[dict[int, str], dict[int, str]]
preview_map[page_num] = thumb_url
else:
# Legacy normal previews (CSS sprite sheets with gdtm class)
# Store as "url|offsetX|width|height" for frontend to render
# Store as "url|offsetX|offsetY|width|height" for frontend to render
for match in _NORMAL_PREVIEW_RE.finditer(html):
sprite_url = match.group(1)
offset_x = int(match.group(2))
width = int(match.group(3))
height = int(match.group(4))
page_num = int(match.group(5))
preview_map[page_num] = f"{sprite_url}|{offset_x}|{width}|{height}"
offset_y = int(match.group(3))
width = int(match.group(4))
height = int(match.group(5))
page_num = int(match.group(6))
preview_map[page_num] = f"{sprite_url}|{offset_x}|{offset_y}|{width}|{height}"

return token_map, preview_map

Expand Down
20 changes: 17 additions & 3 deletions pwa/src/app/browse/[gid]/[token]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
import { RatingStars } from '@/components/RatingStars'
import { toast } from 'sonner'
import { t } from '@/lib/i18n'
import { ArrowLeft, ChevronDown, ChevronUp } from 'lucide-react'

Check warning on line 11 in pwa/src/app/browse/[gid]/[token]/page.tsx

View workflow job for this annotation

GitHub Actions / Frontend Lint

'ChevronUp' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 11 in pwa/src/app/browse/[gid]/[token]/page.tsx

View workflow job for this annotation

GitHub Actions / Frontend Lint

'ChevronDown' is defined but never used. Allowed unused vars must match /^_/u
import type { EhComment } from '@/lib/types'

Check warning on line 12 in pwa/src/app/browse/[gid]/[token]/page.tsx

View workflow job for this annotation

GitHub Actions / Frontend Lint

'EhComment' is defined but never used. Allowed unused vars must match /^_/u

// ── Preview grid with scaled sprite offsets ─────────────────────────────

Expand All @@ -17,7 +17,7 @@
thumbs,
onRead,
}: {
thumbs: { page: number; url: string; isSprite: boolean; offsetX?: number; width?: number; height?: number }[]
thumbs: { page: number; url: string; isSprite: boolean; offsetX?: number; offsetY?: number; width?: number; height?: number }[]
onRead: (page: number) => void
}) {
const gridRef = useRef<HTMLDivElement>(null)
Expand All @@ -44,6 +44,14 @@
const th = thumb.height ?? 300
const scale = cellSize.w ? cellSize.w / tw : 1
const scaledH = th * scale
const normalizedOffsetX = (() => {
const raw = thumb.offsetX ?? 0
return raw > 0 ? -raw : raw
})()
const normalizedOffsetY = (() => {
const raw = thumb.offsetY ?? 0
return raw > 0 ? -raw : raw
})()
return (
<button
key={thumb.page}
Expand All @@ -56,7 +64,7 @@
className="w-full h-full"
style={{
backgroundImage: `url(${thumb.url})`,
backgroundPosition: `${(thumb.offsetX ?? 0) * scale}px center`,
backgroundPosition: `${normalizedOffsetX * scale}px ${normalizedOffsetY * scale}px`,
backgroundSize: `auto ${scaledH}px`,
backgroundRepeat: 'no-repeat',
}}
Expand Down Expand Up @@ -243,19 +251,25 @@
url: string
isSprite: boolean
offsetX?: number
offsetY?: number
width?: number
height?: number
}[] = []
for (let i = 1; i <= count; i++) {
const raw = previewData.previews[String(i)]
if (!raw) continue
if (raw.includes('|')) {
const [spriteUrl, ox, w, h] = raw.split('|')
const parts = raw.split('|')
const [spriteUrl, ox, oy, w, h] =
parts.length >= 5
? parts
: [parts[0], parts[1], '0', parts[2], parts[3]]
thumbs.push({
page: i,
url: api.eh.thumbProxyUrl(spriteUrl),
isSprite: true,
offsetX: parseInt(ox),
offsetY: parseInt(oy),
width: parseInt(w),
height: parseInt(h),
})
Expand Down
17 changes: 11 additions & 6 deletions pwa/src/components/Reader/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ interface ReaderProps {
images: GalleryImage[]
totalPages: number
initialPage?: number
/** EH preview thumbnail map: { "1": "url" or "url|ox|w|h" } */
/** EH preview thumbnail map: { "1": "url" or "url|ox|oy|w|h" (legacy: oy omitted) } */
previews?: Record<string, string>
}

Expand Down Expand Up @@ -629,7 +629,7 @@ interface ThumbnailStripProps {
images: ReaderImage[]
currentPage: number
onPageSelect: (page: number) => void
/** Preview thumbs from EH CDN: { "1": "url" or "url|ox|w|h" } */
/** Preview thumbs from EH CDN: { "1": "url" or "url|ox|oy|w|h" (legacy: oy omitted) } */
previews?: Record<string, string>
}

Expand Down Expand Up @@ -679,14 +679,19 @@ function ThumbnailStrip({ images, currentPage, onPageSelect, previews }: Thumbna
const parts = previewRaw.split('|')
const spriteUrl = parts[0]
const ox = Number(parts[1])
const cellW = Number(parts[2]) || 200
const cellH = Number(parts[3]) || 300
const hasOffsetY = parts.length >= 5
const oy = Number(hasOffsetY ? parts[2] : 0)
const cellW = Number(parts[hasOffsetY ? 3 : 2]) || 200
const cellH = Number(parts[hasOffsetY ? 4 : 3]) || 300
const normalizedOffsetX = ox > 0 ? -ox : ox
const normalizedOffsetY = oy > 0 ? -oy : oy
// Scale based on width only — backend normalizes sprite heights.
const scale = 48 / cellW
const scaledOx = ox * scale
const scaledOx = normalizedOffsetX * scale
const scaledOy = normalizedOffsetY * scale
spriteStyle = {
backgroundImage: `url(/api/eh/thumb-proxy?url=${encodeURIComponent(spriteUrl)})`,
backgroundPosition: `${scaledOx}px center`,
backgroundPosition: `${scaledOx}px ${scaledOy}px`,
backgroundSize: `auto ${cellH * scale}px`,
backgroundRepeat: 'no-repeat',
width: '100%',
Expand Down
2 changes: 1 addition & 1 deletion pwa/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export interface EhFavoritesResult {
export interface EhImageMap {
gid: number
images: Record<string, string> // { "1": "image_page_token", ... }
previews: Record<string, string> // { "1": "thumb_url" or "sprite_url|offsetX|w|h", ... }
previews: Record<string, string> // { "1": "thumb_url" or "sprite_url|offsetX|offsetY|w|h" (legacy: offsetY omitted), ... }
}

// ── Download ──────────────────────────────────────────────────────────
Expand Down
Loading